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

來源:互聯網
上載者:User

VUE 源碼分析簡介Vue 是 MVVM 架構中的新貴,如果我沒記錯的話作者應該畢業不久,現在在google。vue 如作者自己所說,在api設計上受到了很多來自knockout、angularjs等大牌架構影響,但作者相信 vue 在效能、易用性方面是有優勢。同時也自己做了和其它架構的效能對比,在這裡。今天以版本 0.10.4 為準 入口Vue 的入口也很直白: 1var demo = new Vue({ el: '#demo', data: { message: 'Hello Vue.js!' } })   和 ko 、avalon 不同的是,vue 在一開始就必須指定 el 。個人認為這裡設計得不是很合理,因為如果一份資料要綁定到兩個不同dom節點上,那就不得不指定一個同時包含了這兩個dom節點的祖先dom節點。接下來去找 Vue 的定義。翻開源碼,vue 用 grunt。build命令中用了作者自己寫的gulp-component來組合程式碼片段。具體請讀者自己看看,這裡不仔細說了。 從 /src/main.js 裡看到,Vue 的定義就是 ViewModal 的定義。開啟 ViewModel,發現它的定義中只是執行個體化了一個 Compiler,把自己作為參數傳給建構函式。同時看到 ViewModel 原型上定義了一些方法,基本上是跟內部事件、dom 操作有關。那接下來我們就主要看看這個 compiler了。不要忘了我們第一個目的是找到它雙工綁定的主要原理。 雙工綁定翻到 compiler 的定義,代碼太長。猶豫了一下決定還是刪掉一些注釋貼出來,因為基本上大部分值得看的都在這裡,願深入的讀者最好看源檔案。  function Compiler (vm, options) {    var compiler = this,        key, i     compiler.init       = true    compiler.destroyed  = false    options = compiler.options = options || {}    utils.processOptions(options)    extend(compiler, options.compilerOptions)    compiler.repeat   = compiler.repeat || false    compiler.expCache = compiler.expCache || {}    var el = compiler.el = compiler.setupElement(options)    utils.log('\nnew VM instance: ' + el.tagName + '\n')         compiler.vm       = el.vue_vm = vm    compiler.bindings = utils.hash()    compiler.dirs     = []    compiler.deferred = []    compiler.computed = []    compiler.children = []    compiler.emitter  = new Emitter(vm)     if (options.methods) {        for (key in options.methods) {            compiler.createBinding(key)        }    }     if (options.computed) {        for (key in options.computed) {            compiler.createBinding(key)        }    }     // VM ---------------------------------------------------------------------     vm.$         = {}    vm.$el       = el    vm.$options  = options    vm.$compiler = compiler    vm.$event    = null     var parentVM = options.parent    if (parentVM) {        compiler.parent = parentVM.$compiler        parentVM.$compiler.children.push(compiler)        vm.$parent = parentVM    }    vm.$root = getRoot(compiler).vm     // DATA -------------------------------------------------------------------    compiler.setupObserver()         var data = compiler.data = options.data || {},        defaultData = options.defaultData    if (defaultData) {        for (key in defaultData) {            if (!hasOwn.call(data, key)) {                data[key] = defaultData[key]            }        }    }     var params = options.paramAttributes    if (params) {        i = params.length        while (i--) {            data[params[i]] = utils.checkNumber(                compiler.eval(                    el.getAttribute(params[i])                )            )        }    }     extend(vm, data)    vm.$data = data    compiler.execHook('created')    data = compiler.data = vm.$data     var vmProp    for (key in vm) {        vmProp = vm[key]        if (            key.charAt(0) !== '$' &&            data[key] !== vmProp &&            typeof vmProp !== 'function'        ) {            data[key] = vmProp        }    }     compiler.observeData(data)     // COMPILE ----------------------------------------------------------------    if (options.template) {        this.resolveContent()    }    while (i--) {        compiler.bindDirective(compiler.deferred[i])    }    compiler.deferred = null     if (this.computed.length) {        DepsParser.parse(this.computed)    }     compiler.init = false    compiler.execHook('ready')}   注釋就已經寫明了 compiler 執行個體化分為四個階段,第一階段是一些基礎的設定。兩個值得注意的點:一是在 compiler 裡面定義一個 vm 屬性來儲存對傳入的 ViewModel 的引用;二是對 method 和 computed 的每一個成員都調用了 createBinding 。跳到 createBinding:  CompilerProto.createBinding = function (key, directive) {    /*省略*/    var compiler = this,        methods  = compiler.options.methods,        isExp    = directive && directive.isExp,        isFn     = (directive && directive.isFn) || (methods && methods[key]),        bindings = compiler.bindings,        computed = compiler.options.computed,        binding  = new Binding(compiler, key, isExp, isFn)     if (isExp) {        /*省略*/    } else if (isFn) {        bindings[key] = binding        binding.value = compiler.vm[key] = methods[key]    } else {        bindings[key] = binding        if (binding.root) {            /*省略*/            if (computed && computed[key]) {                // computed property                compiler.defineComputed(key, binding, computed[key])            } else if (key.charAt(0) !== '$') {                /*省略*/            } else {                /*省略*/            }        } else if (computed && computed[utils.baseKey(key)]) {            /*省略*/        } else {            /*省略*/        }    }    return binding}   它做了兩件事情:一是執行個體化了一個叫做 Bingding 的東西,二是將 method 和 computed 成員的 bingding 進行了一些再處理。憑直覺和之前看過的代碼,我們可以大膽猜測這個執行個體化的 bingding 很可能就是用來儲存資料和相應地"更新回呼函數"的集合。點進 /src/binding 裡。果然,看到其中的 update 、pub 等函數和 sub 、dir 等對象成員,基本證明猜對了。 到這裡,執行個體化的對象已經有點多了。後面還會更多,為了讓各位不迷失,請提前看看這張關鍵對象圖:   看完 bingding,我們繼續回到 createBinding 中,剛才還說到對 method 和 computed 成員的 bingding 做了一些再處理。對 method,就直接在 vm 上增加了一個同名的引用,我們可以把 vm 看做一個公開的載體,在上面做引用就相當於把自己公開了。對 computed 的成員,使用defineComputed 做的處理是:在vm上定義同名屬性,並將 getter/setter 對應到相應computed成員的$get和$set。 至此,compiler 的第一部分做完,基本上把資料的架子都搭好了。我們看到 bingding 的 pub 和 sub, 知道了 vue 也是就與 observe 模式,那接下來就看看它是如何把把視圖編譯成資料更新函數,並註冊到bingding裡。 回到compiler裡,第二部分處理了一下vm,增加了一些引用。 第三部分關鍵的來了,一看就知道最重要的就是第一句 compiler.setupObserver() 和最後一句compiler.observeData(data) 。直接看源碼的讀者,注釋裡已經很清楚了。第一句是用來註冊一些內部事件的。最後一句是用來將資料的成員轉化成 getter/setter。並和剛剛提到的bingding 相互綁定。值得注意的是,如果遇到資料成員是對象或者數組,vue 是遞迴式將它們轉化成 getter/setter 的,所以你嵌套多深都沒關係,直接替換掉這些成員也沒關係,它對新替換的對象重新遞迴式轉化。 這裡的代碼都很易懂,讀者可以自己點進去看。我只想說一點,就是 vue 在內部實現中使用了很多事件派發器,也就是 /src/emitter。比如對資料的 set 操作。在 set 函數只是觸發一個 set 事件,後面的視圖更新函數什麼都是註冊這個事件下的。這個小小的設計讓關鍵的幾個模組解耦得非常好,能夠比較獨立地進行測試。同時也為架構本身的擴充提供了很多很多的空間。下面這張圖展示了對data的成員進行修改時內部的事件派發:   視圖渲染和擴充看到最後一部分視圖渲染,這裡值得注意的是,vue 支援的是 angular 風格的可複用的directive。directive 的具體實現和之前的 ko 什麼的沒太大區別,都是聲明 bind、update等函數。 至於擴充方面,vue已有明確的 component 和 plugin 的概念,很好理解,讀者看看文檔即可。 另外注意下,vue 是到最後才處理 computed 和普通資料的依賴關係的。 總結總體來說,vue 在核心架構上很精巧。精指的是沒有像ko一樣先實現一些強大但複雜的資料結構,而是需要什麼就實現什麼。巧指的是在代碼架構上既完整實現了功能,又盡量地解耦,為擴充提供了很大的空間。比如它使用了 binding 這樣一個中間體,而不是將試圖更新函數直接註冊到資料的set函數中等等,這些設計都是值得學習了。 當然我們也看到了一些有異議的地方: 比如是否考慮將資料的轉化和視圖編譯明確分成兩個過程?這樣容易實現資料的複用,也就是最開始講的問題。這樣改的話,compiler 的執行個體化的代碼也可以稍微更優雅一些:先處理資料和依賴關係,再建立bingding並綁定各種事件,最後處理視圖。  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.