MVVM大比拼之knockout.js源码精析
简介
本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档。
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源码精析的更多相关文章
- MVVM大比拼之avalon.js源码精析
简介 avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特点是: 使用 observe 模式,性能高. 将原始对象用object.defineProperty重写,不需要用户像用knoc ...
- MVVM大比拼之vue.js源码精析
VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多来自knockout.angularj ...
- vue.js源码精析
MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ...
- MVVM大比拼之AngularJS源码精析
MVVM大比拼之AngularJS源码精析 简介 AngularJS的学习资源已经非常非常多了,AngularJS基础请直接看官网文档.这里推荐几个深度学习的资料: AngularJS学习笔记 作者: ...
- MVVM架构~knockoutjs系列之从Knockout.Validation.js源码中学习它的用法
返回目录 说在前 有时,我们在使用一个插件时,在网上即找不到它的相关API,这时,我们会很抓狂的,与其抓狂,还不如踏下心来,分析一下它的源码,事实上,对于JS这种开发语言来说,它开发的插件的使用方法都 ...
- Vue.js 源码分析(一) 代码结构
关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...
- 深入理解unslider.js源码
最近用到了一个挺好用的幻灯片插件,叫做unslider.js,就想看看怎么实现幻灯片功能,就看看源码,顺便自己也学习学习.看完之后收获很多,这里和大家分享一下. unslider.js 源码和使用教程 ...
- Jquery.cookie.js 源码和使用方法
jquery.cookie.js源码和使用方法 jQuery操作cookie的插件,大概的使用方法如下 $.cookie(‘the_cookie’); //读取Cookie值$.cookie(’the ...
- basket.js 源码分析
basket.js 源码分析 一.前言 basket.js 可以用来加载js脚本并且保存到 LocalStorage 上,使我们可以更加精准地控制缓存,即使是在 http 缓存过期之后也可以使用.因此 ...
随机推荐
- MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息
MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二 ...
- Enterprise Solution 3.1 企业应用开发框架 .NET ERP/CRM/MIS 开发框架,C/S架构,SQL Server + ORM(LLBL Gen Pro) + Infragistics WinForms
行业:基于数据库的制造行业管理软件,包含ERP.MRP.CRM.MIS.MES等企业管理软件 数据库平台:SQL Server 2005或以上 系统架构:C/S 开发技术 序号 领域 技术 1 数据库 ...
- 开源一个跨平台运行的服务插件 - TaskCore.MainForm
本次将要很大家分享的是一个跨平台运行的服务插件 - TaskCore.MainForm,此框架是使用.netcore来写的,现在netcore已经支持很多系统平台运行了,所以将以前的Task.Main ...
- javascript arguments(转)
什么是arguments arguments 是是JavaScript里的一个内置对象,它很古怪,也经常被人所忽视,但实际上是很重要的.所有主要的js函数库都利用了arguments对象.所以agru ...
- Android studio使用gradle动态构建APP(不同的包,不同的icon、label)
最近有个需求,需要做两个功能相似的APP,大部分代码是一样的,只是界面不一样,以前要维护两套代码,比较麻烦,最近在网上找资料,发现可以用gradle使用同一套代码构建两个APP.下面介绍使用方法: 首 ...
- 关系型数据库与NoSQL数据库
关系型数据库的优缺点 优点: 可以做事务处理,从而保证了数据的一致性: 可以进行JOIN等多表查询: 由于以SQL标准化为前提,数据更新的开销很小(相同的字段基本上都只有一处). 缺点: 大量数据的写 ...
- TCP/IP之Nagle算法与40ms延迟
Nagle算法是针对网络上存在的微小分组可能会在广域网上造成拥塞而设计的.该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组确认到达之前不能发送其他的小分组.同时,TCP收集这 ...
- ubuntu14 查找并删除所有文件名中带有特定关键词的文件
http://askubuntu.com/questions/625219/how-to-search-and-delete-files-who-contain-specific-string-in- ...
- 解除win7网络限速.
在电脑刚买或者系统重装了的时候,win7系统会默认限制20%的网络速度,限制了我们的上网速度,我们可以解决这个限制,让上网变得更快 下面是操作步骤 1.开始>运行 2.输入以下命令,然后确定 g ...
- lucene 基础知识点
部分知识点的梳理,参考<lucene实战>及网络资料 1.基本概念 lucence 可以认为分为两大组件: 1)索引组件 a.内容获取:即将原始的内容材料,可以是数据库.网站(爬虫).文本 ...