用KnockoutJS实现ToDoMVC代码分析

体验地址

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>
View Code

 

核心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();

}());
View Code

 

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/

你可能感兴趣的:(knockout)