简介

本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档。

knockout.js (以下简称 ko )是最早将 MVVM 引入到前端的重要功臣之一。目前版本已更新到 3 。相比同类主要有特点有:

  • 双工绑定基于 observe 模式,性能高。

  • 插件和扩展机制非常完善,无论在数据层还是展现层都能满足各种复杂的需求。

  • 向下支持到IE6

  • 文档、测试完备,社区较活跃。

入口

以下分析都将对照 github 上3.x的版本。有一点需要先了解:ko 使用 google closure compiler 进行压缩,因为 closure compiler 会在压缩时按一定规则改变代码本身,所以 ko 源码中有很多类似ko.exportSymbol('subscribable', ko.subscribable) 的语句来防止压缩时引用丢失。愿意深入了解的读者可以自己先去读一下 closure compiler,不了解也可以跳过。

启动代码示例:

var App = function(){
this.firstName = ko.observable('Planet');
this.lastName = ko.observable('Earth');
this.fullName = ko.computed({
read: function () {
return this.firstName() + " " + this.lastName();
},
write: function (value) {
var lastSpacePos = value.lastIndexOf(" ");
if (lastSpacePos > 0) {
this.firstName(value.substring(0, lastSpacePos));
this.lastName(value.substring(lastSpacePos + 1));
}
},
owner: this
});
} ko.applyBindings(new App,document.getElementById('ID'))

  

直接翻到源码 /src/subscribables/observable.js 第一行。

ko.observable = function (initialValue) {
var _latestValue = initialValue; function observable() {
if (arguments.length > 0) {
// Write
// Ignore writes if the value hasn't changed
if (observable.isDifferent(_latestValue, arguments[0])) {
observable.valueWillMutate();
_latestValue = arguments[0];
if (DEBUG) observable._latestValue = _latestValue;
observable.valueHasMutated();
} return this; // Permits chained assignments
}
else {
// Read
ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation
return _latestValue;
}
}
ko.subscribable.call(observable);
ko.utils.setPrototypeOfOrExtend(observable, ko.observable['fn']); if (DEBUG) observable._latestValue = _latestValue;
/**这里省略了专为 closure compiler 写的语句**/
return observable; }

这就是knockout核心 ,observable对象的定义。可以看到这个函数最后返回了一个也叫做 observable 的函数,也就是用户定义值的读写器(accessor)。让我们可以通过 app.firstName() 来读属性,用app.firstName('William') 来写属性。源码还通过 ko.subscribable.call(observable); 使这个函数有了被订阅的功能,让 firstName 在改变时能通知所有订阅了它的对象。可以简单猜想,这个订阅功能的实现,其实就只是维护了一个回调函数的队列,当自己的值改变时,就执行这些回调函数。根据上面的代码,我们可以猜测回调函数应 该是在 observable.valueHasMutated(); 执行的,稍后验证。

除此之外这里只有一点要注意的,就是 ko.dependencyDetection.registerDependency(observable);这是之后实现订阅的核心,稍后细讲。

我们再看 ko 如何将数据绑定到页面元素上,翻到 /src/binding/bindingAttrbuteSyntax.js 426行:

ko.applyBindings = function (viewModelOrBindingContext, rootNode) {
if (!jQuery && window['jQuery']) {
jQuery = window['jQuery'];
} if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
rootNode = rootNode || window.document.body;
applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
};

 

刚开始可能觉得长函数名不太好读,但习惯之后注释都可以不用看了。从这里可以看到源码创造了一个叫做 bingdingContext 的东西,并且开始和节点及其子节点绑定。我们先不继续深入,到这里可以先看一眼 ko 的整体机制了,为了之后能清楚知道讲到哪里了。



数据依赖实现

我们现在重新回过头来看 启动代码和 observable 的代码。启动代码中通过 computed 定义的属性被 ko 称为computed observables(我们暂且称为"计算属性") (示例中的fullName),特点是它的值是依赖于其他普通属性的,当其他的属性的值发生变化时,它也应该自动发生变化。我们在刚才 observable 的代码中看到 普通属性 已经有了 subscribe 的功能。那么我们只需要根据 计算属性 的定义函数来生成一个 更新计算属性值 的函数,并将它注册到它所依赖的普通属性(示例中的 firstName 和 lastName )的回调队列就行了,然后等着普通属性修改时调用这个回调函数。这些机制都很简单,接下来的问题是,我们怎么知道 计算属性 依赖哪些 普通属性 ?还记得刚才代码中的ko.dependencyDetection.registerDependency(observable);吗?这是写在属性被读取的函数里的。我们不难想到,我们只要执行一下计算属性的定义函数,其中被依赖的普通属性就会被读到。如果我们在执行计算属性定义函数之前,把生成的计算属性更新函数放到一个第三方作用域中保存起来,在普通属性被读到时,再去这个作用域中取出这个更新函数放到自己的subsrcibe队列中,不就实现了计算属性对普通属性的订阅了吗?翻到这个registerDependency的源码中去,/src/subscribables/dependencyDetection.js

registerDependency: function (subscribable) {
if (currentFrame) {
if (!ko.isSubscribable(subscribable))
throw new Error("Only subscribable things can act as dependencies");
currentFrame.callback(subscribable, subscribable._id || (subscribable._id = getId()));
}
},

  

发现里面有一个私有变量 currentFrame,猜想应该是用来保存计算属性的更新函数的。在看 compute 的定义函数,/src/subscribables/dependencyObservable.js 第一行,不要被代码长度和长函数名吓到,直接翻到最后的return值,和普通属性一样返回了一个函数,叫做dependentObservable。很明显,它也是一个读写器。我们继续往下看那些主动执行的语句,目的是找到它是否在刚才第三方的 currentFrame 中注册了自己的更新函数。在233行找到 evaluateImmediate()。再看这个函数的定义,果然在 81 行找到了 :

ko.dependencyDetection.begin({
callback: function(subscribable, id) {
if (!_isDisposed) {
if (disposalCount && disposalCandidates[id]) {
_subscriptionsToDependencies[id] = disposalCandidates[id];
++_dependenciesCount;
delete disposalCandidates[id];
--disposalCount;
} else {
addSubscriptionToDependency(subscribable, id);
}
}
},
computed: dependentObservable,
isInitial: !_dependenciesCount
});

 

ko.dependencyDetection.begin 并在其中注册了一个回调函数和一些相关属性。我们去看 这个begin 函数的定义:

function begin(options) {
outerFrames.push(currentFrame);
currentFrame = options;
}

  

果然,这些注册的东西就是被保存到了currentFrame里面。至此,计算属性的实现机制就已经理清楚了,即:

先将自己的更新函数及相关信息注册到第三方作用域中,再立即执行自己的定义函数。当被依赖的属性在定义函数中被读取时,它们会去第三方用域中取出 当前计算属性 的更新函数等信息,并注册到自己的回调列表中去。这其实是一种被动注册的过程。

双工绑定

为什么先要讲数据依赖呢,因为konckout源码的精彩之处正在于此。实际上,我们完全可以把计算属性和普通属性的这套实现机制应用到视图元素与数据之间,我们把视图元素也看做一个计算属性不就行了吗?我们生成一个更新视图的函数,注册到所依赖的数据回调中不就行了吗。对应到之前的applyBindings代码和图。我们先看ko生成的那个BindingContext是什么? 通过 getBindingContext 我们发现它返回了个 bindingContext 的实例。找到定义函数,略过上面函数定义,我们找到最关键的76行,这里使用 ko.dependentObservable(如果你还有印象,这个函数就是computed的别名)生成那个一个计算属性。这个计算属性的定义函数是 updateContext,我们再来看这个函数的定义,里面往当前实例的成员里填充了一些作用域相关的数据,如$parent、$root等。并且它读取传入的数据(之后称为ViewModel)的相关属性,意味着只要ViewModel有变化,它也会自动变化。我们可以这样理解,视图除了需要数据本身外,常常还需要一些其他信息,比如上级作用域等等,因此创造了一个bingdingContext对象,它不仅能完美随着数据变化而变化,还包含了其他信息以供视图使用。之后我们只要把视图函数的更新函数注册到这个对象的回调队列里就好了。

好,我们回到源码看看真实实现,还是回到applyBindings函数,开始看applyBindingsToNodeAndDescendantsInternal函数。跟着直觉都应该知道主线在 225 行的

applyBindingsToNodeInternal函数。继续跳,274行。记住刚才传递给这个函数的值,node就是一个视图node,sourceBindings是null,bindingContext就是之前生成的。这里源码比较复杂了,读者最好自己也对照一下源码。读到这里要重新强调了一下了,我们当前的目的是挖掘节点是如何和bingdingContext进行绑定的。不妨先自己想想。我们回顾一下 ko 在节点进行绑定的语法是什么样的 :

  

<div data-bind="text : c,visible: shouldShowMessage""></div>

这个节点上有两个绑定,一个是text一个是visible。他们以 , 分割,并且对应不同的ViewModel属性。那么我们肯定要通过词法解析或其他手段从节点的data-bind中取出这些绑定信息,然后一个一个将相应的视图更新函数注册到相应的属性回调队列中。看源码:

300 行又得到一个计算属性bindingsUpdater(这时候已经不是什么属性了,不过我们暂时还是这样称呼吧)。

var bindingsUpdater = ko.dependentObservable(
function() {
bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
// Register a dependency on the binding context to support obsevable view models.
if (bindings && bindingContext._subscribable)
bindingContext._subscribable();
return bindings;
},
null, { disposeWhenNodeIsRemoved: node } );

它的定义函数中通过 getBindings 函数读到了 bingdingContext。并且赋值给 bingdings。看注释你也知道了这个bindings保存的就是节点上的绑定信息。这里插入一下,你应该已经发现 ko 代码里广泛地用到了dependentObservable ,实际上,你只要想让什么数据和其他数据保持更新联动,你就可以通过它来实现。比如这段代码就把bingdings这个变量和bindingContext关联起来了。如果你想再把什么数据和bindings绑定起来,只要使用dependentObservable注册一个函数,并在函数读到bindingsUpdater就行了。一个简单地机制,构建了一个多么精彩的世界。

好了,继续往下看,345行有个 forEach,应该就是为把每一个绑定和相应地属性绑在一起了。果然,如果你仔细看了ko文档里关于自定义banding的章节,你应该一看到handler['init']和handler['update']就明白了。正是这里,bingding通过init函数将node的变化映射到数据变化上,再将数据变化通过dependentObservable和node的update绑定起来。

至此,视图到数据,数据到视图的双工引擎搞定!

其他

看完双工模型,再对着ko的文档看看它的插件机制,你应该已经能很轻松地运用把它了。推荐读者再自己看看它对数组数据的处理。对数组的和嵌套对象的处理一直是MVVM在性能等方面的一大课题。我之后在其他框架源码分析中也会讲到。ko在这方面实现上并无亮点,读者自己看看就好。

总体来说,ko的文档、注释之完备,源码之精彩可谓业界楷模。聊以此文抛砖引玉,与君共赏。明天将带来avalon源码精析,敬请期待。

MVVM大比拼之knockout.js源码精析的更多相关文章

  1. MVVM大比拼之avalon.js源码精析

    简介 avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特点是: 使用 observe 模式,性能高. 将原始对象用object.defineProperty重写,不需要用户像用knoc ...

  2. MVVM大比拼之vue.js源码精析

    VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多来自knockout.angularj ...

  3. vue.js源码精析

    MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ...

  4. MVVM大比拼之AngularJS源码精析

    MVVM大比拼之AngularJS源码精析 简介 AngularJS的学习资源已经非常非常多了,AngularJS基础请直接看官网文档.这里推荐几个深度学习的资料: AngularJS学习笔记 作者: ...

  5. MVVM架构~knockoutjs系列之从Knockout.Validation.js源码中学习它的用法

    返回目录 说在前 有时,我们在使用一个插件时,在网上即找不到它的相关API,这时,我们会很抓狂的,与其抓狂,还不如踏下心来,分析一下它的源码,事实上,对于JS这种开发语言来说,它开发的插件的使用方法都 ...

  6. Vue.js 源码分析(一) 代码结构

    关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...

  7. 深入理解unslider.js源码

    最近用到了一个挺好用的幻灯片插件,叫做unslider.js,就想看看怎么实现幻灯片功能,就看看源码,顺便自己也学习学习.看完之后收获很多,这里和大家分享一下. unslider.js 源码和使用教程 ...

  8. Jquery.cookie.js 源码和使用方法

    jquery.cookie.js源码和使用方法 jQuery操作cookie的插件,大概的使用方法如下 $.cookie(‘the_cookie’); //读取Cookie值$.cookie(’the ...

  9. basket.js 源码分析

    basket.js 源码分析 一.前言 basket.js 可以用来加载js脚本并且保存到 LocalStorage 上,使我们可以更加精准地控制缓存,即使是在 http 缓存过期之后也可以使用.因此 ...

随机推荐

  1. 哪种缓存效果高?开源一个简单的缓存组件j2cache

    背景 现在的web系统已经越来越多的应用缓存技术,而且缓存技术确实是能实足的增强系统性能的.我在项目中也开始接触一些缓存的需求. 开始简单的就用jvm(java托管内存)来做缓存,这样对于单个应用服务 ...

  2. hadoop 2.7.3本地环境运行官方wordcount

    hadoop 2.7.3本地环境运行官方wordcount 基本环境: 系统:win7 虚机环境:virtualBox 虚机:centos 7 hadoop版本:2.7.3 本次先以独立模式(本地模式 ...

  3. C#异步编程(二)

    async和await结构 序 前篇博客异步编程系列(一) 已经介绍了何谓异步编程,这篇主要介绍怎么实现异步编程,主要通过C#5.0引入的async/await来实现. BeginInvoke和End ...

  4. VC中的MFC到底是什么?

    1. 微软基础类库(英语:Microsoft Foundation Classes,简称MFC)是一个微软公司提供的类库(class libraries),以C++类的形式封装了Windows API ...

  5. 免费公开课,讲解强大的文档集成组件Aspose,现在可报名

    课程①:Aspose.Total公开课内容:讲解全能型文档管理工具Aspose.Total主要功能及应用领域时间:2016-11-24 14:30 (暂定)报名地址:http://training.e ...

  6. 编译器开发系列--Ocelot语言4.类型定义的检查

    这里主要介绍一下检查循环定义的结构体.联合体.是对成员中包含自己本身的结构体.联合体进行检查.所谓"成员中包含自己本身",举例来说,就是指下面这样的定义. struct point ...

  7. 解决maven下载jar慢的问题(如何更换Maven下载源)

    修改 配置文件 maven 安装 路径 F:\apache-maven-3.3.9\conf 修改 settings.xml 在 <mirrors> <!-- mirror | Sp ...

  8. Python学习基础

    1.使用范围: 大数据 .图像处理.web .运维.爬虫.自动化.科学计算 2.准备环境: linux/mac python 3.5.2 ipython vim/sublime/atom 3.列表 3 ...

  9. 使用apache自带日志分割模块rotatelogs,分割日志

    rotatelogs 是 Apache 2.2 中自带的管道日志程序,参数如下(参见:http://lamp.linux.gov.cn/Apache/ApacheMenu/programs/rotat ...

  10. git图像化界面GUI的使用

    GIT学习笔记 一.        基础内容 1.git是一个版本控制软件,与svn类似,特点是分布式管理,不需要中间总的服务器,可以增加很多分支. 2.windows下的git叫msysgit,下载 ...