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 缓存过期之后也可以使用.因此 ...
随机推荐
- 避免重复造轮子的UI自动化测试框架开发
一懒起来就好久没更新文章了,其实懒也还是因为忙,今年上半年的加班赶上了去年一年的加班,加班不息啊,好了吐槽完就写写一直打算继续的自动化开发 目前各种UI测试框架层出不穷,但是万变不离其宗,驱动PC浏览 ...
- JavaScript自定义浏览器滚动条兼容IE、 火狐和chrome
今天为大家分享一下我自己制作的浏览器滚动条,我们知道用css来自定义滚动条也是挺好的方式,css虽然能够改变chrome浏览器的滚动条样式可以自定义,css也能够改变IE浏览器滚动条的颜色.但是css ...
- 关于全局ID,雪花(snowflake)算法的说明
上次简单的说一下:http://www.cnblogs.com/dunitian/p/6041745.html#uid C#版本的国外朋友已经封装了,大家可以去看看:https://github.co ...
- 这些.NET开源项目你知道吗?.NET平台开源文档与报表处理组件集合(三)
在前2篇文章这些.NET开源项目你知道吗?让.NET开源来得更加猛烈些吧 和这些.NET开源项目你知道吗?让.NET开源来得更加猛烈些吧!(第二辑)中,大伙热情高涨.再次拿出自己的私货,在.NET平台 ...
- jQuery.template.js 简单使用
之前看了一篇文章<我们为什么要尝试前后端分离>,深有同感,并有了下面的评论: 我最近也和前端同事在讨论这个问题,比如有时候前端写好页面给后端了,然后后端把这些页面拆分成很多的 views, ...
- vue.js学习笔记
有了孩子之后,元旦就哪也去不了了(孩子太小),刚好利用一些时间,来公司充充电补补课,学习学习新技术,在这里做一个整理和总结.(选择的东西,既然热爱就把他做好吧!). 下来进入咱们的学习环节: 一.从H ...
- JavaWeb——Servlet
一.基本概念 Servlet是运行在Web服务器上的小程序,通过http协议和客户端进行交互. 这里的客户端一般为浏览器,发送http请求(request)给服务器(如Tomcat).服务器接收到请求 ...
- java web学习总结(五) -------------------servlet开发(一)
一.Servlet简介 Servlet是sun公司提供的一门用于开发动态web资源的技术. Sun公司在其API中提供了一个servlet接口,用户若想用发一个动态web资源(即开发一个Java程序向 ...
- Android开发学习——画横线竖线
画横线/竖线 竖线 <View android:layout_width="1dp" android:layout_height="match_parent&quo ...
- ubuntu下配置vimtab空格数
vim ~/.vimrc 没有就创建 set tabstop=4 //4就是4个空格