用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/

posted @ 2015-02-11 20:39  GongQi  阅读(1615)  评论(0编辑  收藏  举报