VUE 源碼分析
簡介
Vue 是 MVVM 框架中的新貴,如果我沒記錯的話作者應該畢業不久,現在在google。vue 如作者自己所說,在api設計上受到了很多來自knockout、angularjs等大牌框架影響,但作者相信 vue 在性能、易用性方面是有優勢。同時也自己做了和其它框架的性能對比,在這里。
今天以版本 0.10.4 為準
入口
Vue 的入口也很直白:
var 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并綁定各種事件,最后處理視圖。
這幾天有事去了, 沒按時更新,抱歉。下一期帶來angular源碼分析,敬請期待。
文章列表