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 缓存过期之后也可以使用.因此 ...
随机推荐
- Cocos2d Android 环境搭建
1.在开始之前,需要先准备好资源如下,如果安卓开发环境有了直接装第3.4. 1.JDK 点击下载 (1.6) 2.ADT(已经自带Android SDK)点击下载 3.NDK 点击下载 4. ...
- Android性能优化之利用LeakCanary检测内存泄漏及解决办法
前言: 最近公司C轮融资成功了,移动团队准备扩大一下,需要招聘Android开发工程师,陆陆续续面试了几位Android应聘者,面试过程中聊到性能优化中如何避免内存泄漏问题时,很少有人全面的回答上来. ...
- Mysql基础代码(不断完善中)
Mysql基础代码,不断完善中~ /* 启动MySQL */ net start mysql /* 连接与断开服务器 */ mysql -h 地址 -P 端口 -u 用户名 -p 密码 /* 跳过权限 ...
- Html 制作相册
本文主要讲述采用Html5+jQuery+CSS 制作相册的小小记录. 主要功能点: Html5进行布局 调用jQuery(借用官网的一句话:The Write Less, Do More)极大的简化 ...
- HTML5笔记2——HTML5音/视频标签详解
音视频的发展史 早期:<embed>+<object>+文件 问题:不是所有浏览器都支持,而且embed不是标准. 现状:Realplay.window media.Quick ...
- BPM配置故事之案例3-参与者与数据自动加载
这才过了两天,阿海又来了. 阿海:公司决定改进管理方式,以后物资申请的申请人和申请部门要写具体使用人的名字和部门了. 小明:不是要让我改回去吧? 阿海:那太麻烦了,你能不能把申请人改成选择,选好人自动 ...
- 用Java代码实现拦截区域网数据包
起因: 吃饭的时间在想如果区域网内都是通过路由器上网,那如何实现拦截整个区域网的数据包,从而实现某种窥探欲. 思路: 正常是通过电脑网卡预先设置或分配的IP+网关对路由器进行通讯,比如访问百 ...
- 编译器开发系列--Ocelot语言7.中间代码
Ocelot的中间代码是仿照国外编译器相关图书Modern Compiler Implementation 中所使用的名为Tree 的中间代码设计的.顾名思义,Tree 是一种树形结构,其特征是简单, ...
- iOS系列教程 目录 (持续更新...)
前言: 听说搞iOS的都是高富帅,身边妹子无数.咱也来玩玩.哈哈. 本篇所有内容使用的是XCode工具.Swift语言进行开发. 我现在也是学习阶段,每一篇内容都是经过自己实际编写完一遍之后,发现 ...
- WebAPI 2参数绑定方法
简单类型参数 Example 1: Sending a simple parameter in the Url [RoutePrefix("api/values")] public ...