一、整体结构
项目github地址https://github.com/tastejs/todomvc/
排除通用的css样式文件和引用的js库文件,仅看html和js
1.1 knockoutjs版todo app文件结构
knockoutjs
--index.html
--js
----app.js
1.2 backbonejs版todo app文件结构
backbonejs
--index.html
--js
----collections
------todos.js
----models
------todo.js
----routers
------router.js
----views
------app-view.js
------todo-view.js
----app.js
1.3 angularjs版todo app文件结构
angularjs
--index.html
--js
----controllers
------todoCtrl.js
----directives
------todoEscape.js
----services
------todoStorage.js
----app.js
二、knockout版todo主要内容
knockout版todo app实现细节,之前有文讲过,详情见《用KnockoutJS实现ToDoMVC代码分析》
从上文的文件结构可知,其业务代码只有app.js,html view只有index.html
2.1 视图代码index.html
knockout在html原有属性基础上,新增了data-bind属性
data-bind属性作为knockout与html交互的入口,内置了如下若干指令
- visible binding
- text binding
- html binding
- css binding
- style binding
- attr binding
除了上述内置指令,knockout也可以添加自定义指令,如html中出现的enterKey、escapeKey和selectAndFocus指令


<section id="todoapp"><header id="header"><h1>todos</h1><input id="new-todo" data-bind="value: current, valueUpdate: 'afterkeydown', 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, valueUpdate: 'afterkeydown', enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }"></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</button></footer></section>
2.2 业务代码app.js
app.js中,首先对html view中自定义的指令enterKey、escapeKey和selectAndFocus做了定义


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 keywrappedHandler = 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 bindingnewValueAccessor = 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 keyko.bindingHandlers.enterKey = keyhandlerBindingFactory(ENTER_KEY);// another custom binding, this time to handle the escape keyko.bindingHandlers.escapeKey = keyhandlerBindingFactory(ESCAPE_KEY);// wrapper to hasFocus that also selects text and applies focus asyncko.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 focussetTimeout(function () {ko.bindingHandlers.hasFocus.update(element, valueAccessor);}, 0);}};
然后定义了todo model
// represent a single todo itemvar Todo = function (title, completed) {this.title = ko.observable(title);this.completed = ko.observable(completed);this.editing = ko.observable(false);};
ViewModel中定义了html view中的业务方法和属性


// our main view modelvar ViewModel = function (todos) {// map array of passed in todos to an observableArray of Todo objectsthis.todos = ko.observableArray(todos.map(function (todo) {return new Todo(todo.title, todo.completed);}));// store the new todo value being enteredthis.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 pressedthis.add = function () {var current = this.current().trim();if (current) {this.todos.push(new Todo(current));this.current('');}}.bind(this);// remove a single todothis.remove = function (todo) {this.todos.remove(todo);}.bind(this);// remove all completed todosthis.removeCompleted = function () {this.todos.remove(function (todo) {return todo.completed();});}.bind(this);// edit an itemthis.editItem = function (item) {item.editing(true);item.previousTitle = item.title();}.bind(this);// stop editing an item. Remove the item, if it is now emptythis.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 manuallyif (title !== trimmedTitle) {item.title(trimmedTitle);}if (!trimmedTitle) {this.remove(item);}}.bind(this);// cancel editing an item and revert to the previous contentthis.cancelEditing = function (item) {item.editing(false);item.title(item.previousTitle);}.bind(this);// count of all completed todosthis.completedCount = ko.computed(function () {return this.todos().filter(function (todo) {return todo.completed();}).length;}.bind(this));// count of todos that are not completethis.remainingCount = ko.computed(function () {return this.todos().length - this.completedCount();}.bind(this));// writeable computed observable to handle marking all complete/incompletethis.allCompleted = ko.computed({//always return true/false based on the done flag of all todosread: 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 markupthis.getLabel = function (count) {return ko.utils.unwrapObservable(count) === 1 ? 'item' : 'items';}.bind(this);// internal computed observable that fires whenever anything changes in our todosko.computed(function () {// store a clean copy to local storage, which also creates a dependency on the observableArray and all observables in each itemlocalStorage.setItem('todos-knockoutjs', ko.toJSON(this.todos));alert(1);}.bind(this)).extend({rateLimit: { timeout: 500, method: 'notifyWhenChangesStop' }}); // save at most twice per second};
定义完成后,通过下述代码,将ViewModel和view绑定起来
// bind a new instance of our view model to the pagevar viewModel = new ViewModel(todos || []);ko.applyBindings(viewModel);
存储使用的是localStorage
// check local storage for todosvar todos = ko.utils.parseJson(localStorage.getItem('todos-knockoutjs'));
页面链接路由用到了第三方插件
// set up filter routing/*jshint newcap:false */Router({ '/:filter': viewModel.showMode }).init();
三、backbone版todo主要内容
从前文的文件结构中可以发现,backbone版todo app包含index.html和collection部分、model部分、router部分、view部分这些子模块js以及主模块app.js
3.1 html文件index.html


<section id="todoapp"><header id="header"><h1>todos</h1><input id="new-todo" placeholder="What needs to be done?" autofocus></header><section id="main"><input id="toggle-all" type="checkbox"><label for="toggle-all">Mark all as complete</label><ul id="todo-list"></ul></section><footer id="footer"></footer></section><footer id="info"><p>Double-click to edit a todo</p><p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a></p><p>Part of <a href="http://todomvc.com">TodoMVC</a></p></footer><script type="text/template" id="item-template"><div class="view"><input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>><label><%- title %></label><button class="destroy"></button></div><input class="edit" value="<%- title %>"></script><script type="text/template" id="stats-template"><span id="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span><ul id="filters"><li><a class="selected" href="#/">All</a></li><li><a href="#/active">Active</a></li><li><a href="#/completed">Completed</a></li></ul><% if (completed) { %><button id="clear-completed">Clear completed (<%= completed %>)</button><% } %></script>
index.html中主要部分内容很简洁,上述片段中还包含了两个模板的定义,如果只看html部分,内容更少
<section id="todoapp"><header id="header"><h1>todos</h1><input id="new-todo" placeholder="What needs to be done?" autofocus></header><section id="main"><input id="toggle-all" type="checkbox"><label for="toggle-all">Mark all as complete</label><ul id="todo-list"></ul></section><footer id="footer"></footer></section>
上述html中,只有最基本的html元素和属性
backbone没有对html添加扩展属性,对html是没有侵入的
todo对象的列表,也页面底部的状态过滤链接,是通过view template插入到html中的
3.2 各个js文件分析
app.js作为backbone 业务代码主模块,内容很简单,在页面加载完之后,对AppView进行了实例化
/*global $ */ /*jshint unused:false */ var app = app || {}; var ENTER_KEY = 13; var ESC_KEY = 27;$(function () {'use strict';// kick things off by creating the `App`new app.AppView(); });
app-view.js是应用顶层的view,处理的对象是todo model的集合
在app-view.js代码中,首先指定了视图的作用对象和模板对象
然后在events对象中,为dom元素特定事件绑定事件处理函数
在initialize对象中,为todos集合绑定特定事件的事件处理函数
在render函数中,用模板对象渲染指定dom元素
随后依次定义事件处理函数
和app-view.js不同,todo-view.js是负责处理todo list中单个todo对象的dom处理
todo-view.js中代码过程与app-view.js中大致相似
更多view内容可参考What is a view?
todo.js定义了todo对象模型,而todos.js中定义了todo对象模型的集合
前文knockout版本todo app中,也有相应的todo对象和todos对象集合
相比knockout版本中的对象和集合,backbone版本中独立出model和collection模块的意义是什么呢
答案是backbone中model和collection功能比knockout中丰富的多
model是js应用的核心,包括基础的数据以及围绕着这些数据的逻辑:数据转换、验证、属性计算和访问控制
collection是model对象的集合,为model对象提供便捷的操作。在我看来,collection不是必须的,他属于语法糖类型的东西。
更多model和collection内容可以参考
Backbone入门指南(四):Model(数据模型)
Backbone入门指南(五):Collection (数据模型集合)
router.js是根据backbone内置的路由模块实现的路由处理,根据All、Active、Completed三个不同链接,进行不同操作
router使用可以参考认识 Backbone(三) : 什么是 Router
四、angular版todo主要内容
angular版本todo app包含index.html view文件和controller部分、director部分、service部分和主入口app.js
4.1 index.html分析


<ng-view /><script type="text/ng-template" id="todomvc-index.html"><section id="todoapp"><header id="header"><h1>todos</h1><form id="todo-form" ng-submit="addTodo()"><input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus></form></header><section id="main" ng-show="todos.length" ng-cloak><input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)"><label for="toggle-all">Mark all as complete</label><ul id="todo-list"><li ng-repeat="todo in todos | filter:statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo == editedTodo}"><div class="view"><input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="toggleCompleted(todo)"><label ng-dblclick="editTodo(todo)">{{todo.title}}</label><button class="destroy" ng-click="removeTodo(todo)"></button></div><form ng-submit="saveEdits(todo, 'submit')"><input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEdits(todo)" ng-blur="saveEdits(todo, 'blur')" todo-focus="todo == editedTodo"></form></li></ul></section><footer id="footer" ng-show="todos.length" ng-cloak><span id="todo-count"><strong>{{remainingCount}}</strong><ng-pluralize count="remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize></span><ul id="filters"><li><a ng-class="{selected: status == ''} " href="#/">All</a></li><li><a ng-class="{selected: status == 'active'}" href="#/active">Active</a></li><li><a ng-class="{selected: status == 'completed'}" href="#/completed">Completed</a></li></ul><button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed ({{completedCount}})</button></footer></section><footer id="info"><p>Double-click to edit a todo</p><p>Credits:<a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,<a href="http://ericbidelman.com">Eric Bidelman</a>,<a href="http://jacobmumm.com">Jacob Mumm</a> and<a href="http://igorminar.com">Igor Minar</a></p><p>Part of <a href="http://todomvc.com">TodoMVC</a></p></footer></script>
查看index.html发现,body元素下,第一行元素为
<ng-view />
随后,在脚本<script type="text/ng-template" id="todomvc-index.html"></script>中,定义了app的html
html属性中,看到很多ng开头的属性,如ng-app,ng-submit,ng-model等
这些属性,都是angular对html的扩展,而上述属性中大部分是angular内置的指令
todo-escape,todo-focus这两个不是以ng开头的指令,是app自定义的指令
对ng-view指令的用法,更多内容可参考AngularJS Views
4.2 js业务代码分析
angular程序的启动开始于ng-app指令,他的位置也决定了脚本的作用域范围
<body ng-app="todomvc">
这里注册的todomvc模块,与app.js中定义的模块是一致的
angular.module('todomvc', ['ngRoute']).config(function ($routeProvider) {'use strict';var routeConfig = {controller: 'TodoCtrl',templateUrl: 'todomvc-index.html',resolve: {store: function (todoStorage) {// Get the correct module (API or localStorage).return todoStorage.then(function (module) {module.get(); // Fetch the todo records in the background.return module;});}}};$routeProvider.when('/', routeConfig).when('/:status', routeConfig).otherwise({redirectTo: '/'});});
应用程序入口,app.js中,定义了todomvc模块,引入了ngRoute模块
程序中,采用$routeProvider服务对页面路由进行了配置,指定链接对应的配置中,控制器是 TodoCtrl,模板地址是todomvc-index.html,定义了resolve对象,根据todoStorage服务,获取todos集合,填充store对象
关于这里的路由配置中,配置对象和resolve用法,可以参考Promise/Q和AngularJS中的resolve
todoCtrl.js是应用的控制器部分,控制器是和应用的与对象scope交互的地方,可以将一个独立视图的业务逻辑封装在一个独立的容器中。
index.html的模板中,涉及的属性和方法,都是在todoCtrl.js中定义的
todoFocus.js和todoEscape.js是两个自定义指令,对应todo-focus和todo-escape
这里的自定义指令,实际上可以对应到knockout的custom binding,均是对内置指令的扩展
对指令的使用可参考《AngularJS》5个实例详解Directive(指令)机制
todoStorage.js是应用的服务部分,服务部分提供了http服务和localStorage两种方式,并且提供的是promise的异步处理方式
对promise的介绍和对angular中$q服务的介绍可以参考
五、总结
单以此todo app来看knockout、backbone和angular
文件结构上
knockout最简洁,angular其次,backbone最复杂
对html侵入上
backbone对html无侵入,knockout增加了data-bind属性,angular增加了一套属性并提供自定义属性方法
对第三方插件依赖上
knockout不提供dom操作,不提供路由操作,提供简单的模板支持
backbone不提供dom操作,提供了路由模块,依赖underscore函数库
angular提供内置的jqlite,提供路由服务,异步处理服务,依赖服务
代码分离角度
个人认为backbone和angular都比较清晰,knockout一般