MVVM大比拼之knockout.js源码精析

简介

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

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

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

  • 插件和扩展机制很是完善,不管在数据层仍是展示层都能知足各类复杂的需求。node

  • 向下支持到IE6git

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

入口

如下分析都将对照 github 上3.x的版本。有一点须要先了解:ko 使用 google closure compiler 进行压缩,由于 closure compiler 会在压缩时按必定规则改变代码自己,因此 ko 源码中有不少相似ko.exportSymbol('subscribable', ko.subscribable) 的语句来防止压缩时引用丢失。愿意深刻了解的读者能够本身先去读一下 closure compiler,不了解也能够跳过。数组

启动代码示例:app

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(); 执行的,稍后验证。ide

除此以外这里只有一点要注意的,就是 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源码精析,敬请期待。

相关文章
相关标签/搜索