用KnockoutJS实现ToDoMVC代码分析

页面导航:首页 > 网络编程 > JavaScript > 用KnockoutJS实现ToDoMVC代码分析

用KnockoutJS实现ToDoMVC代码分析

来源: 作者: 时间:2016-02-03 09:20 【

HTML View以下是html view中主要部分section id=todoapp header id=header h1todos h1 input id=new-todo data-bind=value: current, enterK
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 to
        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>
Tags:

文章评论

最 近 更 新
热 点 排 行
Js与CSS工具
代码转换工具

<