MVVM大比拼之avalon.js源碼精析

簡介javascript

avalon是國內 司徒正美 寫的MVVM框架,相比同類框架它的特色是:html

  • 使用 observe 模式,性能高。
  • 將原始對象用object.defineProperty重寫,不須要用戶像用knockout時那樣顯示定義各類屬性。
  • 對低版本的IE使用了VBScript來兼容,一直兼容到IE6。

須要看基礎介紹的話建議直接看司徒的博客。在網上搜了一圈,發現已經有了avalon很好的源碼分析,這裏也推薦一下:地址。 avalon在圈子裏一直被詬病不夠規範的問題,請各位沒必要再留言在我這裏,看源碼無非是取其精華去其糟粕。能夠點評,但老是討論用哪一個框架好哪一個很差就沒什麼意義了,如果本身把握不住,用什麼都很差。vue

今天的分析以 avalon.mobile 1.2.5 爲準,avalon.mobile 是專門爲高級瀏覽器準備的,不兼容IE8如下。java

入口node

仍是先看啓動代碼數組

avalon.ready(function() {
    avalon.define("box", function(vm) {
        vm.w = 100;
        vm.h = 100;
        vm.area = function(){
        	get : function(){ return this.w * this.h }
        }
        vm.logW =function(){ console.log(vm.w)}
        
    })
    avalon.scan()
})

  

仍是兩件事:定義viewModel 和 執行掃描。 翻到define 定義:瀏覽器

avalon.define = function(id, factory) {
        if (VMODELS[id]) {
            log("warning: " + id + " 已經存在於avalon.vmodels中")
        }
        var scope = {
            $watch: noop
        }
        factory(scope) //獲得全部定義
        var model = modelFactory(scope) //偷天換日,將scope換爲model
        stopRepeatAssign = true
        factory(model)
        stopRepeatAssign = false
        model.$id = id
        return VMODELS[id] = model
    }

  

其實已經能夠一眼看明白了。這裏只提一點,爲何要執行兩次factory?建議讀者先本身想一下。我這裏直接說出來了: 由於modelFactory中,若是屬性是函數,就會被直接複製到新的model上,但函數內的vm卻仍然指向的原來的定義函數的中的vm,所以發生錯誤。因此經過二次執行factory來修正引用錯誤。
那爲何不在modelFactory中直接就把經過Function.bind或其餘方法來把引用給指定好呢?並且能夠在經過scope得到定之後就直接把 scope 對象修改爲viewModel就行了啊?
這裏的代碼寫法實際上是直接從avalon兼容IE的完整版中搬出來的,由於對老瀏覽器要創造VBScript對象,因此只能先傳個scope進去獲取定義,在根據定義去創造。而且老的瀏覽器也不支持bind等方法。 仍是老規矩,咱們先看看總體機制圖:ruby

雙工引擎app

接下來就是直接一探 modelFactory 內部了。翻到代碼 324 行。框架

function modelFactory(scope, model) {
        if (Array.isArray(scope)) {
            var arr = scope.concat()//原數組的做爲新生成的監控數組的$model而存在
            scope.length = 0
            var collection = Collection(scope)
            collection.push.apply(collection, arr)
            return collection
        }
        if (typeof scope.nodeType === "number") {
            return scope
        }
        var vmodel = {} //要返回的對象
        model = model || {} //放置$model上的屬性
        var accessingProperties = {} //監控屬性
        var normalProperties = {} //普通屬性
        var computedProperties = [] //計算屬性
        var watchProperties = arguments[2] || {} //強制要監聽的屬性
        var skipArray = scope.$skipArray //要忽略監控的屬性
        for (var i = 0, name; name = skipProperties[i++]; ) {
            delete scope[name]
            normalProperties[name] = true
        }
        if (Array.isArray(skipArray)) {
            for (var i = 0, name; name = skipArray[i++]; ) {
                normalProperties[name] = true
            }
        }
        for (var i in scope) {
            loopModel(i, scope[i], model, normalProperties, accessingProperties, computedProperties, watchProperties)
        }
        vmodel = Object.defineProperties(vmodel, descriptorFactory(accessingProperties)) //生成一個空的ViewModel
        for (var name in normalProperties) {
            vmodel[name] = normalProperties[name]
        }
        watchProperties.vmodel = vmodel
        vmodel.$model = model
        vmodel.$events = {}
        vmodel.$id = generateID()
        vmodel.$accessors = accessingProperties
        vmodel[subscribers] = []
        for (var i in Observable) {
            vmodel[i] = Observable[i]
        }
        Object.defineProperty(vmodel, "hasOwnProperty", {
            value: function(name) {
                return name in vmodel.$model
            },
            writable: false,
            enumerable: false,
            configurable: true
        })
        for (var i = 0, fn; fn = computedProperties[i++]; ) { //最後強逼計算屬性 計算本身的值
            Registry[expose] = fn
            fn()
            collectSubscribers(fn)
            delete Registry[expose]
        }
        return vmodel
    }

  

前面聲明瞭一對變量做爲容器,用來保存轉換過的 控制屬性(至關於ko中的observable) 和 計算屬性(至關於ko中的computed) 等等。往下翻到最關鍵的352行,這個 loopModel 函數就是用來生成好各個屬性的入口了。繼續深刻:

function loopModel(name, val, model, normalProperties, accessingProperties, computedProperties, watchProperties) {
        model[name] = val
        if (normalProperties[name] || (val && val.nodeType)) { //若是是元素節點或在全局的skipProperties裏或在當前的$skipArray裏
            return normalProperties[name] = val
        }
        if (name[0] === "$" && !watchProperties[name]) { //若是是$開頭,而且不在watchProperties裏
            return normalProperties[name] = val
        }
        var valueType = getType(val)
        if (valueType === "function") { //若是是函數,也不用監控
            return normalProperties[name] = val
        }
        var accessor, oldArgs
        if (valueType === "object" && typeof val.get === "function" && Object.keys(val).length <= 2) {
            var setter = val.set,
                    getter = val.get
            accessor = function(newValue) { //建立計算屬性,因變量,基本上由其餘監控屬性觸發其改變
                var vmodel = watchProperties.vmodel
                var value = model[name],
                        preValue = value

              if (arguments.length) {
                    if (stopRepeatAssign) {
                        return
                    }

                    if (typeof setter === "function") {
                        var backup = vmodel.$events[name]
                        vmodel.$events[name] = [] //清空回調,防止內部冒泡而觸發屢次$fire
                        setter.call(vmodel, newValue)
                        vmodel.$events[name] = backup
                    }
                    if (!isEqual(oldArgs, newValue)) {
                        oldArgs = newValue
                        newValue = model[name] = getter.call(vmodel)//同步$model
                        withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循環綁定中的代理VM

                      notifySubscribers(accessor) //通知頂層改變
                        safeFire(vmodel, name, newValue, preValue)//觸發$watch回調
                    }
                } else {
                    if (avalon.openComputedCollect) { // 收集視圖刷新函數
                        collectSubscribers(accessor)
                    }
                    newValue = model[name] = getter.call(vmodel)
                    if (!isEqual(value, newValue)) {
                        oldArgs = void 0
                        safeFire(vmodel, name, newValue, preValue)
                    }
                    return newValue
                }
            }
            computedProperties.push(accessor)
        } else if (rchecktype.test(valueType)) {
            accessor = function(newValue) { //子ViewModel或監控數組
                var realAccessor = accessor.$vmodel, preValue = realAccessor.$model
                if (arguments.length) {
                    if (stopRepeatAssign) {
                        return
                    }

                  if (!isEqual(preValue, newValue)) {

                      newValue = accessor.$vmodel = updateVModel(realAccessor, newValue, valueType)
                        var fn = rebindings[newValue.$id]
                        fn && fn()//更新視圖
                        var parent = watchProperties.vmodel
                        withProxyCount && updateWithProxy(parent.$id, name, newValue)//同步循環綁定中的代理VM
                        model[name] = newValue.$model//同步$model
                        notifySubscribers(realAccessor)   //通知頂層改變
                        safeFire(parent, name, model[name], preValue)   //觸發$watch回調
                    }
                } else {
                    collectSubscribers(realAccessor) //收集視圖函數
                    return realAccessor
                }
            }
            accessor.$vmodel = val.$model ? val : modelFactory(val, val)
            model[name] = accessor.$vmodel.$model
        } else {
            accessor = function(newValue) { //簡單的數據類型
                var preValue = model[name]
                if (arguments.length) {
                    if (!isEqual(preValue, newValue)) {
                        model[name] = newValue //同步$model
                        var vmodel = watchProperties.vmodel
                        withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循環綁定中的代理VM
                        notifySubscribers(accessor) //通知頂層改變
                        safeFire(vmodel, name, newValue, preValue)//觸發$watch回調
                    }
                } else {
                    collectSubscribers(accessor) //收集視圖函數
                    return preValue
                }
            }
            model[name] = val
        }
        accessor[subscribers] = [] //訂閱者數組
        accessingProperties[name] = accessor
    }

  

源碼的註釋其實已經寫得很是清楚了,若是你看過我上一篇對knockout源碼的解讀,你會發現avalon這裏面的機制和knockout幾乎是同樣的。函數無非就是根據定義函數中各個屬性的類型來生成讀寫器(accessor),這個讀寫器會用在後面的 defineProperty 中。這裏惟一值得提一下的就是那個 updateWithProxy 函數。只有一種狀況須要用到它,就是當頁面上使用了 ms-repeat 或者其餘循環綁定來處理 數組或對象 時,會生爲循環中的對象生成一個代理對象,這個代理對象記錄除數據自己外和做用於相關的一些變量,和knockout的bindingContext有些像。 好了,到這裏源碼基本上沒什麼難度,咱們來作一點有意思的事情。還記得以前咱們提出的關於 執行兩次 factory的 疑問嗎?第二次執行主要是爲了修正函數屬性中的引用,咱們看上面這代碼中,但屬性的類型是function時,就直接複製,若是咱們對這個函數執行一下bind的方法呢,是否是就不用使用factory修正引用了?來試一下,先將 318 行的二次執行factory註釋掉。再loopModel函數中 424 行改爲

            

return normalProperties[name] = val.bind(model)

  

咱們寫個頁面載入改過的avalon,而後跑一下這段測試:

var vma = avalon.define('a',function(vm){

    vm.a = "a"

    vm.b = "b"

    vm.c = {

        get : function(){return this.a+this.b}

    }

    vm.c2 = {

        get : function(){return vm.a+vm.b}

    }

    vm.d = function(){

        return this.a+this.b //注意這裏用的是 this

    }

})

vma.a = "c"

console.log(vma.c == vma.a+vma.b)

console.log(vma.d() == vma.a+vma.b)

  

有沒有驗證,結果你們最好本身試驗一下。 這裏能夠看到,若是隻是針對現代瀏覽器,avalon的內核仍是有不少能夠重構的地方的。

viewModel的內部實現已經搞清,接下來就只剩看看如何處理和頁面元素的綁定了。翻到 1214 行scan函數的定義,主要是執行了 scanTag 。再看,主要是執行了 scanAttr。再看,終於找到了和 knockout 看起來同樣的 bindingHandlers 了,再往下翻翻就會發現和 knockout 是同樣的綁定機制了。讀者能夠本身看,看不懂的地方翻翻我上一篇中ko的一樣部分看看就知道了。

其餘

最後仍是講講對數組的處理。以前在ko中咱們看到ko爲對象專門準備了一個observableArray,裏面重寫pop等方法,以保證在處理函數時能只通知改動元素相關的綁定,而不用修改整個數組綁定的視圖。在avalon中,咱們看到在 loopModel 467行的 rchecktype.test(valueType) 這個語句。rchecktype 是個正則 /^(?:object|array)$/ ,也就是判斷該屬性是否是對象或數組。若是是,在 491 行 的

accessor.$vmodel = val.$model ? val : modelFactory(val, val)

又生成一個modelFactory,這時傳入modelFactory的第一個參數就多是數組了,再看modelFacotry 定義,當第一個函數爲數組時,將其變成了一個Collection對象,而Collection也是重寫了各類數組方法。果真,機制你們都差很少。不過司徒在博客中強調了它的數組處理效率更高,你們能夠本身看看。

最後推薦兩篇做者的博客文章,看看他在寫MVVM中更多技術細節

迷你MVVM框架 avalonjs 實現上的幾個難點
迷你MVVM框架avalon在兼容舊式IE作的努力

仍是那句話,取其精華。明天將帶來MVVM新貴 vue.js 源碼分析,敬請期待。

相關文章
相關標籤/搜索