zoukankan      html  css  js  c++  java
  • 用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/

  • 相关阅读:
    min.js文件 反压缩查看
    Simple HTML DOM解析器 使用php解析网页字符串进行dom操作
    使用clipboard.js实现页面内容复制到剪贴板
    php7微信支付回调失败
    微信卡券开发
    Windows内存性能分析(一)内存泄漏
    LoadRunner添加检查点
    LoadRunner中两种录制模式的区别
    Jmeter参数化_CSV Data Set Config
    APTM敏捷性能测试模型
  • 原文地址:https://www.cnblogs.com/GongQi/p/4284791.html
Copyright © 2011-2022 走看看