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源碼精析,敬請期待。

相關文章
相關標籤/搜索