体验地址

Knockout 版todo web app在线体验

http://todomvc.com/examples/knockoutjs/

源码地址

项目源码地址,此地址包含了各种JS框架实现的todo web app

https://github.com/tastejs/todomvc

HTML View

以下是html view中主要部分

<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" data-bind="value: current, enterKey: add" placeholder="What needs to be done?" autofocus>
</header>
<section id="main" data-bind="visible: todos().length">
<input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: filteredTodos">
<li data-bind="css: { completed: completed, editing: editing }">
<div class="view">
<input class="toggle" data-bind="checked: completed" type="checkbox">
<label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
</div>
<input class="edit" data-bind="value: title, enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus:editing, event: { blur: $root.cancelEditing }">
</li>
</ul>
</section>
<footer id="footer" data-bind="visible: completedCount() || remainingCount()">
<span id="todo-count">
<strong data-bind="text: remainingCount">0</strong>
<span data-bind="text: getLabel(remainingCount)"></span> left
</span>
<ul id="filters">
<li>
<a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'active' }" href="#/active">Active</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'completed' }" href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">
Clear completed (<span data-bind="text: completedCount"></span>)
</button>
</footer>
</section>

核心JS

/*global ko, Router */
(function () {
'use strict'; var ENTER_KEY = 13;
var ESCAPE_KEY = 27; // A factory function we can use to create binding handlers for specific
// keycodes.
function keyhandlerBindingFactory(keyCode) {
return {
init: function (element, valueAccessor, allBindingsAccessor, data, bindingContext) {
var wrappedHandler, newValueAccessor; // wrap the handler with a check for the enter key
wrappedHandler = function (data, event) {
if (event.keyCode === keyCode) {
valueAccessor().call(this, data, event);
}
}; // create a valueAccessor with the options that we would want to pass to the event binding
newValueAccessor = function () {
return {
keyup: wrappedHandler
};
}; // call the real event binding's init function
ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, data, bindingContext);
}
};
} // a custom binding to handle the enter key
ko.bindingHandlers.enterKey = keyhandlerBindingFactory(ENTER_KEY); // another custom binding, this time to handle the escape key
ko.bindingHandlers.escapeKey = keyhandlerBindingFactory(ESCAPE_KEY); // wrapper to hasFocus that also selects text and applies focus async
ko.bindingHandlers.selectAndFocus = {
init: function (element, valueAccessor, allBindingsAccessor, bindingContext) {
ko.bindingHandlers.hasFocus.init(element, valueAccessor, allBindingsAccessor, bindingContext);
ko.utils.registerEventHandler(element, 'focus', function () {
element.focus();
});
},
update: function (element, valueAccessor) {
ko.utils.unwrapObservable(valueAccessor()); // for dependency
// ensure that element is visible before trying to focus
setTimeout(function () {
ko.bindingHandlers.hasFocus.update(element, valueAccessor);
}, 0);
}
}; // represent a single todo item
var Todo = function (title, completed) {
this.title = ko.observable(title);
this.completed = ko.observable(completed);
this.editing = ko.observable(false);
}; // our main view model
var ViewModel = function (todos) {
// map array of passed in todos to an observableArray of Todo objects
this.todos = ko.observableArray(todos.map(function (todo) {
return new Todo(todo.title, todo.completed);
})); // store the new todo value being entered
this.current = ko.observable(); this.showMode = ko.observable('all'); this.filteredTodos = ko.computed(function () {
switch (this.showMode()) {
case 'active':
return this.todos().filter(function (todo) {
return !todo.completed();
});
case 'completed':
return this.todos().filter(function (todo) {
return todo.completed();
});
default:
return this.todos();
}
}.bind(this)); // add a new todo, when enter key is pressed
this.add = function () {
var current = this.current().trim();
if (current) {
this.todos.push(new Todo(current));
this.current('');
}
}.bind(this); // remove a single todo
this.remove = function (todo) {
this.todos.remove(todo);
}.bind(this); // remove all completed todos
this.removeCompleted = function () {
this.todos.remove(function (todo) {
return todo.completed();
});
}.bind(this); // edit an item
this.editItem = function (item) {
item.editing(true);
item.previousTitle = item.title();
}.bind(this); // stop editing an item. Remove the item, if it is now empty
this.saveEditing = function (item) {
item.editing(false); var title = item.title();
var trimmedTitle = title.trim(); // Observable value changes are not triggered if they're consisting of whitespaces only
// Therefore we've to compare untrimmed version with a trimmed one to chech whether anything changed
// And if yes, we've to set the new value manually
if (title !== trimmedTitle) {
item.title(trimmedTitle);
} if (!trimmedTitle) {
this.remove(item);
}
}.bind(this); // cancel editing an item and revert to the previous content
this.cancelEditing = function (item) {
item.editing(false);
item.title(item.previousTitle);
}.bind(this); // count of all completed todos
this.completedCount = ko.computed(function () {
return this.todos().filter(function (todo) {
return todo.completed();
}).length;
}.bind(this)); // count of todos that are not complete
this.remainingCount = ko.computed(function () {
return this.todos().length - this.completedCount();
}.bind(this)); // writeable computed observable to handle marking all complete/incomplete
this.allCompleted = ko.computed({
//always return true/false based on the done flag of all todos
read: function () {
return !this.remainingCount();
}.bind(this),
// set all todos to the written value (true/false)
write: function (newValue) {
this.todos().forEach(function (todo) {
// set even if value is the same, as subscribers are not notified in that case
todo.completed(newValue);
});
}.bind(this)
}); // helper function to keep expressions out of markup
this.getLabel = function (count) {
return ko.utils.unwrapObservable(count) === 1 ? 'item' : 'items';
}.bind(this); // internal computed observable that fires whenever anything changes in our todos
ko.computed(function () {
// store a clean copy to local storage, which also creates a dependency on the observableArray and all observables in each item
localStorage.setItem('todos-knockoutjs', ko.toJSON(this.todos));
alert(1);
}.bind(this)).extend({
rateLimit: { timeout: 500, method: 'notifyWhenChangesStop' }
}); // save at most twice per second
}; // check local storage for todos
var todos = ko.utils.parseJson(localStorage.getItem('todos-knockoutjs')); // bind a new instance of our view model to the page
var viewModel = new ViewModel(todos || []);
ko.applyBindings(viewModel); // set up filter routing
/*jshint newcap:false */
Router({ '/:filter': viewModel.showMode }).init();
}());

JS代码解析

在上述版本todo app中,Todo可视为app的Model,包含3 个属性分别是title,completed,editing,并且这三个属性均被注册为observable的

var Todo = function (title, completed) {
this.title = ko.observable(title);
this.completed = ko.observable(completed);
this.editing = ko.observable(false);
};

在ViewModel中,首先定义了下面属性:

todos,current,showmode,filteredTodos

其中todos被注册为observableArray,并用形参todos,调用map方法为ViewModel的todos属性赋值

current没有被赋值

showmode被初始化为'all'

filteredTodos是一个依赖属性,通过ko.computed()计算获得,其值依赖于todos和showmode属性,根据showmode的值,在todos中选择合适的todo对象

在计算todos和filteredTodos属性时,发现调用了map,filter方法,这些是ECMAScript5中定义的 Array的标准方法,其余常用的还有forEach,every,some等。

this.todos = ko.observableArray(todos.map(function (todo) {
return new Todo(todo.title, todo.completed);
})); // store the new todo value being entered
this.current = ko.observable(); this.showMode = ko.observable('all'); this.filteredTodos = ko.computed(function () {
switch (this.showMode()) {
case 'active':
return this.todos().filter(function (todo) {
return !todo.completed();
});
case 'completed':
return this.todos().filter(function (todo) {
return todo.completed();
});
default:
return this.todos();
}
}.bind(this));

接下来依次为Viewmodel定义了add,remove,removeCompleted,editItem,saveEditing,cancelEditing,completedCount,remainingCount,allCompleted,getLabel方法

其中completedCount,remainingCount,allCompleted也是通过ko.computed()计算得出

与completedCount,remainingCount不同,allCompleted同时定义了read和write方法,在write方法中,将todos集合中各个todo对象的computed属性赋值

下面片段应用到了knockout中一个扩展用法

使用了Rate-limiting observable notifications,来达到在更新发生后的指定时间,来触发ko.computed()中的匿名函数

下面片段中,localStorage在knockout判断所有更新已结束,notifyWhenChangesStop函数触发后的500毫秒后将序列化为JSON对象的todos存到浏览器localStorage中

// internal computed observable that fires whenever anything changes in our todos
ko.computed(function () {
// store a clean copy to local storage, which also creates a dependency on the observableArray and all observables in each item
localStorage.setItem('todos-knockoutjs', ko.toJSON(this.todos));
}.bind(this)).extend({
rateLimit: { timeout: 500, method: 'notifyWhenChangesStop' }
}); // save at most twice per second

在将viewmodel绑定至ko时,以下代码先从localStorage读取,如有则使用,没有则为空,利用localStorage的本地存储功能,已经可以完成一个完整体验的todo app

最下面使用了一个Router来路由All Active Completed三个tab的请求,knockout自身不包含路由模块,这里的Router是由其余模块提供的

// check local storage for todos
var todos = ko.utils.parseJson(localStorage.getItem('todos-knockoutjs')); // bind a new instance of our view model to the page
var viewModel = new ViewModel(todos || []);
ko.applyBindings(viewModel); // set up filter routing
/*jshint newcap:false */
Router({ '/:filter': viewModel.showMode }).init();

说完了todo Model和ViewModel,再来看一下todo app中自定义绑定

在todo app中,分别提供了对键盘回车键ENTER_KEY、取消键ESCAPE_KEY的事件绑定

当为dom元素绑定enter_key、escape_key事件时,会以当前dom元素作用域执行赋予的valueAccessor函数

在selectAndFocus自定义绑定中,同时定义了init方法和update方法,在init中为dom元素注册了foucs方法,在update方法中来触发元素的focus,其目的是为了在选中todo元素,可以立即进入可编辑的状态

// A factory function we can use to create binding handlers for specific
// keycodes.
function keyhandlerBindingFactory(keyCode) {
return {
init: function (element, valueAccessor, allBindingsAccessor, data, bindingContext) {
var wrappedHandler, newValueAccessor; // wrap the handler with a check for the enter key
wrappedHandler = function (data, event) {
if (event.keyCode === keyCode) {
valueAccessor().call(this, data, event);
}
}; // create a valueAccessor with the options that we would want to pass to the event binding
newValueAccessor = function () {
return {
keyup: wrappedHandler
};
}; // call the real event binding's init function
ko.bindingHandlers.event.init(element, newValueAccessor, allBindingsAccessor, data, bindingContext);
}
};
} // a custom binding to handle the enter key
ko.bindingHandlers.enterKey = keyhandlerBindingFactory(ENTER_KEY); // another custom binding, this time to handle the escape key
ko.bindingHandlers.escapeKey = keyhandlerBindingFactory(ESCAPE_KEY); // wrapper to hasFocus that also selects text and applies focus async
ko.bindingHandlers.selectAndFocus = {
init: function (element, valueAccessor, allBindingsAccessor, bindingContext) {
ko.bindingHandlers.hasFocus.init(element, valueAccessor, allBindingsAccessor, bindingContext);
ko.utils.registerEventHandler(element, 'focus', function () {
element.focus();
});
},
update: function (element, valueAccessor) {
ko.utils.unwrapObservable(valueAccessor()); // for dependency
// ensure that element is visible before trying to focus
setTimeout(function () {
ko.bindingHandlers.hasFocus.update(element, valueAccessor);
}, 0);
}
};

HTML View解析

在todo的输入框中,默认绑定了current属性,初始时current默认为空,所以显示的是placeholder中的值

输入框使用上上述自定义绑定,将ViewModel的add方法传给了enterKey

placeholder和autofucus都是HTML5支持的标准属性

<input id="new-todo" data-bind="value: current,  enterKey: add" placeholder="What needs to be done?" autofocus>

下面片段是todo app的展示列表

列表ul元素使用foreach绑定了ViewModel的filteredTodos属性

每一个li对应一个todo对象

随着该todo被勾选为完成与否的变化,该li元素的css class同时发生变化

event:{dbclickL:$root.editItem}即为label元素绑定了双击事件,事件处理函数为ViewModel的editItem方法,而删除按钮绑定了click事件,事件处理函数为ViewModel的remove方法

当双击label元素时,li中的input输入框可见,可以对todo对象进行编辑,这里也采用了自定义绑定,同时绑定了enterKey,escapeKey,selectAndFocus事件,也绑定了标准事件blur,其事件处理函数为ViewModel的cancelEditing(该方法未实现)

<ul id="todo-list" data-bind="foreach: filteredTodos">
<li data-bind="css: { completed: completed, editing: editing }">
<div class="view">
<input class="toggle" data-bind="checked: completed" type="checkbox">
<label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
</div>
<input class="edit" data-bind="value: title, enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus:editing, event: { blur: $root.cancelEditing }">
</li>
</ul>

最后

以上基本解析了knockout版todo app的主要代码逻辑

更多资料,建议去官网学习

地址:http://knockoutjs.com/

用KnockoutJS实现ToDoMVC代码分析的更多相关文章

  1. Android代码分析工具lint学习

    1 lint简介 1.1 概述 lint是随Android SDK自带的一个静态代码分析工具.它用来对Android工程的源文件进行检查,找出在正确性.安全.性能.可使用性.可访问性及国际化等方面可能 ...

  2. pmd静态代码分析

    在正式进入测试之前,进行一定的静态代码分析及code review对代码质量及系统提高是有帮助的,以上为数据证明 Pmd 它是一个基于静态规则集的Java源码分析器,它可以识别出潜在的如下问题:– 可 ...

  3. [Asp.net 5] DependencyInjection项目代码分析-目录

    微软DI文章系列如下所示: [Asp.net 5] DependencyInjection项目代码分析 [Asp.net 5] DependencyInjection项目代码分析2-Autofac [ ...

  4. [Asp.net 5] DependencyInjection项目代码分析4-微软的实现(5)(IEnumerable<>补充)

    Asp.net 5的依赖注入注入系列可以参考链接: [Asp.net 5] DependencyInjection项目代码分析-目录 我们在之前讲微软的实现时,对于OpenIEnumerableSer ...

  5. 完整全面的Java资源库(包括构建、操作、代码分析、编译器、数据库、社区等等)

    构建 这里搜集了用来构建应用程序的工具. Apache Maven:Maven使用声明进行构建并进行依赖管理,偏向于使用约定而不是配置进行构建.Maven优于Apache Ant.后者采用了一种过程化 ...

  6. STM32启动代码分析 IAR 比较好

    stm32启动代码分析 (2012-06-12 09:43:31) 转载▼     最近开始使用ST的stm32w108芯片(也是一款zigbee芯片).开始看他的启动代码看的晕晕呼呼呼的. 还好在c ...

  7. 常用 Java 静态代码分析工具的分析与比较

    常用 Java 静态代码分析工具的分析与比较 简介: 本文首先介绍了静态代码分析的基 本概念及主要技术,随后分别介绍了现有 4 种主流 Java 静态代码分析工具 (Checkstyle,FindBu ...

  8. SonarQube-5.6.3 代码分析平台搭建使用

    python代码分析 官网主页: http://docs.sonarqube.org/display/PLUG/Python+Plugin Windows下安装使用: 快速使用: 1.下载jdk ht ...

  9. angular代码分析之异常日志设计

    angular代码分析之异常日志设计 错误异常是面向对象开发中的记录提示程序执行问题的一种重要机制,在程序执行发生问题的条件下,异常会在中断程序执行,同时会沿着代码的执行路径一步一步的向上抛出异常,最 ...

随机推荐

  1. Windows 8 动手实验系列教程 实验7:磁贴和通知

    动手实验 实验7:磁贴和通知 2012年9月 简介 磁贴是Windows应用商店应用用户体验的重要元素.当应用程序被安装后,它的磁贴将在Windows 8开始屏幕被创建.该磁贴(称为主磁贴)作为启动应 ...

  2. mfc控件与其对应的对象的关联方法

    对话框的控件与其对应类的对象相关联:(两种方法) (1)      通过CWnd::DoDataExchange函数进行关联: 用VC++6.0的MFC ClassWizard中的Member Var ...

  3. 重载(overload),覆盖/重写(override),隐藏(hide)

    写正题之前,先给出几个关键字的中英文对照,重载(overload),覆盖/重写(override),隐藏(hide).在早期的C++书籍中,常常把重载(overload)和覆盖(override)搞错 ...

  4. 基于特定值来推断隐藏显示元素的jQuery插件

    jQuery-Visibly是一款小巧简单的jQuery隐藏显示元素插件.该插件依据某个元素的值,例如以下拉框的值.输入框的值等来推断是否显示某个指定的元素. 用于推断的值能够是单个值,或者是多个值, ...

  5. qt button以及label实现不规则图形(五种方法:使用QSS,设置Mask图片,自己画)

    1.方法1:准备一张边界是透明的不规则图形 QPushButton * pbtn = new QPushButton;    pbtn->setStyleSheet("QPushBut ...

  6. Windows 8 和 Windows 8.1 中对插件和 ActiveX 的支持

    此文章将介绍页面在 Windows 8 适用于桌面版的 Internet Explorer 中与在新 Windows UI 的 Internet Explorer 中的不同表现. Windows 8 ...

  7. BUG系列:转让startActivityForResult()&amp;onActivityResult()没有反应

    前天遇到了一个麻烦,还真是麻烦啊. 我使用startActivityForResult()&onActivityResult().由Activity-A 跳转到Activity-B 页面,然后 ...

  8. VC生成的DLL给QT的EXE调用时lib路径问题小结

    VC生成的DLL给QT调用,有两种方式,一种是隐式调用调用(使用.lib文件方式): ① 在*.pro工程文件中添加VC生成的lib文件路径时,或者使用一个绝对路径,如: LIBS += " ...

  9. Qt 4.7.4 完美动态编译发布动态调试,以及静态编译发布

    首先是准备工作,去QT主页下载独立的QT类库安装包以及完整QT SDK安装包,还有QT Creator for windows 版 下载地址:http://qt.nokia.com/downloads ...

  10. POJ 3040 Allowance 贪心

    这题目的贪心思路还是有一点细节问题的. 还没有证明,据说是因为题目给的条件是每个价格是比它小的价格的倍数才能这么贪心的. 思路如下: 假设要给奶牛的钱为C 1)从大面值到小面值一次拿钱,能拿多少拿多少 ...