博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入浅出KnockoutJS
阅读量:6698 次
发布时间:2019-06-25

本文共 17105 字,大约阅读时间需要 57 分钟。

深入浅出KnockoutJS

  写在前面,本文资料大多来源网上,属于自己的学习笔记整理。

  其中主要内容来自learn.knockoutjs.com,源码解析部分资料来自司徒正美博文《knockout.js学习笔记》系列。


 

1. Knockout初体验

1.1 Before Knockout

  假设我们的页面输入区域有一个div用来展示一件物品的名字,同时有一个输入框用来编辑这件物品的名字

  使用JQuery,上述交互逻辑可以如下实现

var item = {    id: 88,    name: "Apple Pie"};$("#itemName").text(item.name);$("#itemNameEdit").val(item.name).change(function() {    item.name = $(this).val();    $("#itemName").text(item.name);});

采用这种方式的缺点

  • 当UI和data的交互越来越多时,代码量迅速增长到难以维护
  • •Dom Query Based
  • 上述代码耦合度高,不可重用
  • Id、classname命名难以管理

1.2 Use Knockout

  HTML View如下

  Javascript如下

function ViewModel={    this.id=88;    this.name=ko.observable(“Apple”);};ko.applyBindings(new ViewModel());

现在,当输入框中值发生变化时,div中显示的值也会自动发送变化


 

2.  Knockout基础

2.1 MVVM模式

  Knockoutjs遵循Model(M)—View(V)—ViewModel(VM)模式

2.2 单次绑定

  从ViewModel绑定至UI这一层只进行一次绑定,不追踪数据在任何一方的变化,适用于数据展现

  Javascript与Html示例如下

function AppViewModel() {    this.firstName = "Bert";    this.lastName = "Bertington";}ko.applyBindings(new AppViewModel());

First name:

Last name:

  效果如下图所示

 2.3 双向绑定

  无论数据在ViewModel或者是UI中变化,将会更新另一方,最为灵活的绑定方式,同时代价最大

function AppViewModel() {    this.firstName = ko.observable("Bert");    this.lastName = ko.observable("Bertington");}ko.applyBindings(new AppViewModel());

First name:

Last name:

First name:

Last name:

  上述绑定,当输入框中值发生改变时,<p>标签中显示内容相应发生改变

2.4 依赖绑定

  以其它observable的值为基础来组成新的值,新值也是双向绑定的

function AppViewModel() {    this.firstName = ko.observable("Bert");    this.lastName = ko.observable("Bertington");    this.fullName = ko.computed(function() {        return this.firstName() + " " + this.lastName();        }, this);}ko.applyBindings(new AppViewModel());

First name:

Last name:

First name:

Last name:

Full name:

上述代码示例中,fullName依赖于firstName和lastName,改变firstName和lastName任意值,fullName的显示也相应改变

2.5 绑定数组

  可以为属性绑定集合

// Class to represent a row in the seat reservations gridfunction SeatReservation(name, initialMeal) {    var self = this;    self.name = name;    self.meal = ko.observable(initialMeal);}// Overall viewmodel for this screen, along with initial statefunction ReservationsViewModel() {    var self = this;    // Non-editable catalog data - would come from the server    self.availableMeals = [        { mealName: "Standard (sandwich)", price: 0 },        { mealName: "Premium (lobster)", price: 34.95 },        { mealName: "Ultimate (whole zebra)", price: 290 }    ];        // Editable data    self.seats = ko.observableArray([        new SeatReservation("Steve", self.availableMeals[0]),        new SeatReservation("Bert", self.availableMeals[0])    ]);}ko.applyBindings(new ReservationsViewModel());

Your seat reservations

Passenger name Meal Surcharge

  上述代码将seats对象绑定了一个集合对象,在html view中,通过foreach指令渲染视图,效果如下下图

2.6 增加添加和删除元素功能

// Class to represent a row in the seat reservations gridfunction SeatReservation(name, initialMeal) {    var self = this;    self.name = name;    self.meal = ko.observable(initialMeal);    self.formattedPrice = ko.computed(function() {        var price = self.meal().price;        return price;            });    }// Overall viewmodel for this screen, along with initial statefunction ReservationsViewModel() {    var self = this;    // Non-editable catalog data - would come from the server    self.availableMeals = [        { mealName: "Standard (sandwich)", price: 0 },        { mealName: "Premium (lobster)", price: 34.95 },        { mealName: "Ultimate (whole zebra)", price: 290 }    ];        // Editable data    self.seats = ko.observableArray([        new SeatReservation("Steve", self.availableMeals[0]),        new SeatReservation("Bert", self.availableMeals[0])    ]);      // Operations    self.addSeat = function() {        self.seats.push(new SeatReservation("", self.availableMeals[0]));    }    self.removeSeat = function(seat) { self.seats.remove(seat) }}ko.applyBindings(new ReservationsViewModel());

Your seat reservations

Passenger name Meal Surcharge
Remove
  •   上述代码中,为viewmodel添加了addSeat和removeSeat方法。
  •   调用addSeat方法时,为seats集合添加一个初始化SeatReservation对象
  •   调用removeSeat方法时,knockout将当前dom元素绑定的seat对象作为参赛传入到方法中

  效果如图

  更多绑定可访问官网文档,

3. Knockout进阶

3.1 Custom bindings

  •   Binding连接view和viewmodel,除了内置bindings,你可以创建自己的binding

  •   将待注册的绑定,添加为ko.bindingHandlers的属性,然后可以在任意dom元素中使用它
ko.bindingHandlers.yourBindingName = {    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {        // This will be called when the binding is first applied to an element        // Set up any initial state, event handlers, etc. here    },    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {        // This will be called once when the binding is first applied to an element,        // and again whenever any observables/computeds that are accessed change        // Update the DOM element based on the supplied values here.    }};

  custom binding示例

// ----------------------------------------------------------------------------// Reusable bindings - ideally kept in a separate fileko.bindingHandlers.fadeVisible = {    init: function(element, valueAccessor) {        // Start visible/invisible according to initial value        var shouldDisplay = valueAccessor();        $(element).toggle(shouldDisplay);    },    update: function(element, valueAccessor) {        // On update, fade in/out        var shouldDisplay = valueAccessor();        shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();    } };// ----------------------------------------------------------------------------// Page viewmodelfunction Answer(text) { this.answerText = text; this.points = ko.observable(1); }function SurveyViewModel(question, pointsBudget, answers) {    this.question = question;    this.pointsBudget = pointsBudget;    this.answers = $.map(answers, function(text) { return new Answer(text) });    this.save = function() { alert('To do') };                       this.pointsUsed = ko.computed(function() {        var total = 0;        for (var i = 0; i < this.answers.length; i++)            total += this.answers[i].points();        return total;            }, this);}ko.applyBindings(new SurveyViewModel("Which factors affect your technology choices?", 10, [   "Functionality, compatibility, pricing - all that boring stuff",   "How often it is mentioned on Hacker News",       "Number of gradients/dropshadows on project homepage",           "Totally believable testimonials on project homepage"]));

Please distribute points between the following options.

Option Importance

You've used too many points! Please remove some.

You've got points left to use.

  上述代码定义了一个fadeVisible绑定,用来控制元素显示动画效果。init方法根据dom元素传入参数当前状态设置初始显示效果;update方法在pointsUsed 每次发生更新时触发,更新元素显示效果 

3.2 Template binding

  模板绑定用模板的渲染结果来填充关联的DOM元素,构建复制UI架构、可复用、可嵌套
  knockout 支持两种类型模板
  •   Native templating:内置,用于加强控制流程的绑定
  •   String-based templating:集成第三方模板引擎的方式,原理是将model value传递给第三方模板引擎,将结果字符串注入到当前document

  Native templating示例

Participants

Here are the participants:

 3.3 Components and Custom Elements

  组件是将UI代码组织成可复用模块的方法

  使用ko.components.register方法注册组件,组件定义包含viewModel和template

ko.components.register('some-component-name', {    viewModel: 
, template:
});

  一个like/dislike组件示例

ko.components.register('like-widget', {    viewModel: function(params) {        // Data: value is either null, 'like', or 'dislike'        this.chosenValue = params.value;                 // Behaviors        this.like = function() { this.chosenValue('like'); }.bind(this);        this.dislike = function() { this.chosenValue('dislike'); }.bind(this);    },    template:        '
\
\ You
it\
'});function Product(name, rating) { this.name = name; this.userRating = ko.observable(rating || null);} function MyViewModel() { this.products = [ new Product('Garlic bread'), new Product('Pain au chocolat'), new Product('Seagull spaghetti', 'like') // This one was already 'liked' ];} ko.applyBindings(new MyViewModel());

  viewModel中,为products单项绑定了一个Product集合,并为第三个Product对象userRating属性设置为like

  html view中,使用like-widget指令使用上述定义的组件

  效果如下图

4. Knockout实战

4.1 knockout版todo app

  效果如下,在线体验http://todomvc.com/examples/knockoutjs/
  观察各项功能,可以对这一todo app做出如下分析
  •   需要一个todo对象作为 Model
  •   需要一个todos 的集合用来存储各个todo对象
  •   需要filterTodos对象,根据All,Active,Completed过滤todos集合
  •   需要添加、删除、编辑、清除等各种事件方法
  同时,为了良好体验,还可以提供自定义绑定,提供对键盘快捷键的支持,如按下回车键时保存
  为了体验的完整,还需要使用localstorage将todos存储至本地
  

4.2 todo app主要代码分析

  •   Todo Model,包含3 个属性分别是title,completed,editing
// 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);    };
  •   todos Array、filteredTodos
// 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));
  •   Events binding
  •   Custom binding

  提供了对键盘回车键ENTER_KEY、取消键ESCAPE_KEY的事件绑定

  当为dom元素绑定enter_key、escape_key事件时,会以当前dom元素作用域执行赋予的valueAccessor函数

 

  在selectAndFocus自定义绑定中,同时定义了init方法和update方法

  在init中为dom元素注册了foucs方法,在update方法中来触发元素的focus,其目的是为了在选中todo元素,可以立即进入可编辑的状态

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 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 focus        setTimeout(function () {            ko.bindingHandlers.hasFocus.update(element, valueAccessor);        }, 0);    }};
  •   HTML View

 

5. Knockout源码解析

5.1 ko.observable是什么

this.firstName=ko.observable(“Bert”);this.firstName();this.firstName(“test”);

  调用上面代码发生了什么

$.observable = function(value){    var v = value;//将上一次的传参保存到v中,ret与它构成闭包    function ret(neo){        if(arguments.length){ //setter            if(v !== neo ){                v = neo;            }            return ret;        }else{                //getter            return v;        }    }    return ret}

5.2 ko.computed是什么

this.fullName = ko.computed(function() {        return this.firstName() + " " + this.lastName();        }, this);
$.computed = function(obj, scope){    //computed是由多个$.observable组成    var getter, setter    if(typeof obj == "function"){        getter = obj    }else if(obj && typeof obj == "object"){        getter = obj.getter;        setter = obj.setter;        scope  = obj.scope;    }    var v    var ret = function(neo){        if(arguments.length ){            if(typeof setter == "function"){
//setter不一定存在的 if(v !== neo ){ setter.call(scope, neo); v = neo; } } return ret; }else{ v = getter.call(scope); return v; } } return ret;}

5.3 属性依赖如何实现

  调用observable中getter方法时,ret函数对象收集所有对自身的依赖对象

  调用observable中setter方法时,ret函数对象想依赖对象发生通知

  

  调用computed中getter方法时,ret函数对象将自身传递给依赖探测的begin方法

  然后通过call()方法获取函数值,这时,会触发observable中相对应的getter的调用,从而收集到computed中的ret函数对象

  在调用完成后,再将自身移除

$.dependencyDetection = (function () {    var _frames = [];    return {        begin: function (ret) {            _frames.push(ret);        },        end: function () {            _frames.pop();        },        collect: function (self) {            if (_frames.length > 0) {                self.list = self.list || [];                var fn = _frames[_frames.length - 1];                if ( self.list.indexOf( fn ) >= 0)                    return;                self.list.push(fn);            }        }    };})();$.valueWillMutate = function(observable){    var list = observable.list    if($.type(list,"Array")){        for(var i = 0, el; el = list[i++];){            el();        }    }}

5.4 双向绑定如何实现

The name is 
$.buildEvalWithinScopeFunction =  function (expression, scopeLevels) {    var functionBody = "return (" + expression + ")";    for (var i = 0; i < scopeLevels; i++) {        functionBody = "with(sc[" + i + "]) { " + functionBody + " } ";    }    return new Function("sc", functionBody);}$.applyBindings = function(model, node){              var nodeBind = $.computed(function (){        var str = "{" + node.getAttribute("data-bind")+"}"        var fn = $.buildEvalWithinScopeFunction(str,2);        var bindings = fn([node,model]);        for(var key in bindings){            if(bindings.hasOwnProperty(key)){                var fn = $.bindingHandlers["text"]["update"];                var observable = bindings[key]                $.dependencyDetection.collect(observable);//绑定viewModel与UI                fn(node, observable)            }        }    },node);    return nodeBind     }$.bindingHandlers = {}$.bindingHandlers["text"] = {    'update': function (node, observable) {        var val = observable()        if("textContent" in node){            node.textContent = val;        }    }}window.onload = function(){    var model = new MyViewModel();    var node = document.getElementById("node");    $.applyBindings(model, node);}

   上述代码中,$.buildEvalWithinScopeFunction(str,2)返回一个匿名函数

function anonymous(sc/**/) {
with(sc[1]) { with(sc[0]) { return ({text: fullName}) } } }

  通过var bindings = fn([node,model]),bindings得到一个{text:fullName函数对象}的对象,其中,fullName是一个组合依赖属性,即fullName是一个computed中ret函数对象

6. 总结

6.1 优点

  • 专注于data-binding,UI自动刷新,model依赖跟踪
  • 简单易上手,学习成本低
  • 轻量,方便与其他第三方JS框架集成
  • 可扩展,支持自定义定制
  • 浏览器兼容度高,几乎支持所有现代浏览器

6.2 不足

  • 是一个MVVM library,不是一个前端解决方案
  • 缺少Router等重要模块支持
  • 缺少可测试性支持

参考资料

Knockoutjs Tutorial  http://learn.knockoutjs.com
knockout.js学习笔记  http://www.cnblogs.com/rubylouvre/archive/2012/06/17/2551907.html
Knockout todo mvc源码  https://github.com/tastejs/todomvc
 

如果您觉得本文对您有帮助,请【推荐】,谢谢。

转载于:https://www.cnblogs.com/GongQi/p/4284798.html

你可能感兴趣的文章
喜大普奔,Ant Design of Vue 1.0版本发布
查看>>
存储系统设计——NVMe SSD性能影响因素一探究竟
查看>>
高性能的视差动画【译】
查看>>
Scrapy爬虫以及Scrapyd爬虫部署
查看>>
有何建议给即将步入职场的计算机应届毕业生?
查看>>
Android NDK开发之旅2 C语言基础 指针
查看>>
选择了软件测试,你后悔吗?
查看>>
个人Feed流方案演进
查看>>
iOS GCD入门和GCD对CPU多核的使用
查看>>
Java集合——LinkedHashMap
查看>>
从另一个思路来学习安卓事件分发机制
查看>>
你有搭建内部组件平台的需求吗?
查看>>
js 算法2
查看>>
在kotlin中使用dagger2遇到的一个问题
查看>>
iOS 的 NSNotificationCenter 中哪些通知由系统自动发送?
查看>>
Android性能优化,Startalk会话页GIF内存优化实践
查看>>
mac 写php时出现问题: Warning: Cannot modify header information - headers already
查看>>
TCP IP之路由算法
查看>>
服务器与客户端的实时通信
查看>>
NEO改进协议提案4(NEP-4)
查看>>