源碼版本:v2.1.10javascript
經過閱讀源碼,對 Vue2 的基礎運行機制有所瞭解,主要是:vue
Vue2 中數據綁定的實現方式java
Vue2 中對 Virtual DOM 機制的使用方式node
項目構建配置文件爲 build/config.js
,定位 vue.js 對應的入口文件爲 src/entries/web-runtime-with-compiler.js
,基於 rollup 進行模塊打包。git
代碼中使用 flow 進行接口類型標記和檢查,在打包過程當中移除這些標記。爲了閱讀代碼方便,在 VS Code 中安裝了插件 Flow Language Support,而後關閉工做區 JS 代碼檢查,這樣界面就清爽不少了。github
Vue 應用啓動通常是經過 new Vue({...})
,因此,先從該構造函數着手。web
注:本文只關注 Vue 在瀏覽器端的應用,不涉及服務器端代碼。數組
文件:src/core/instance/index.js
瀏覽器
該文件只是構造函數,Vue 原型對象的聲明分散在當前目錄的多個文件中:服務器
init.js:._init()
state.js:.$data
.$set()
.$delete()
.$watch()
render.js:._render()
...
events.js:.$on()
.$once()
.$off()
.$emit()
lifecycle.js:._mount()
._update()
.$forceUpdate()
.$destroy()
構造函數接收參數 options
,而後調用 this._init(options)
。
._init()
中進行初始化,其中會依次調用 lifecycle、events、render、state 模塊中的初始化函數。
Vue2 中應該是爲了代碼更易管理,Vue 類的定義分散到了上面的多個文件中。
其中,對於 Vue.prototype
對象的定義,經過 mixin 的方式在入口文件 core/index.js
中依次調用。對於實例對象(代碼中一般稱爲 vm
)則經過 init 函數在 vm._init()
中依次調用。
文件:src/core/index.js
這裏調用了 initGlobalAPI()
來初始化 Vue 的公共接口,包括:
Vue.util
Vue.set
Vue.delete
Vue.nextTick
Vue.options
Vue.use
Vue.mixin
Vue.extend
asset相關接口:配置在 src/core/config.js
中
調用 new Vue({...})
後,在內部的 ._init()
的最後,是調用 .$mount()
方法來「啓動」。
在 web-runtime-with-compiler.js
和 web-runtime.js
中,定義了 Vue.prototype.$mount()
。不過兩個文件中的 $mount()
最終調用的是 ._mount()
內部方法,定義在文件 src/core/instance/lifecycle.js
中。
Vue.prototype._mount(el, hydrating)
簡化邏輯後的僞代碼:
vm = this vm._watcher = new Watcher(vm, updateComponent)
接下來看 Watcher
。
文件:src/core/observer/watcher.js
先看構造函數的簡化邏輯:
// 參數:vm, expOrFn, cb, options this.vm = vm vm._watchers.push(this) // 解析 options,略.... // 屬性初始化,略.... this.getter = expOrFn // if `function` this.value = this.lazy ? undefined : this.get()
因爲缺省的 lazy
屬性值爲 false
,接着看 .get()
的邏輯:
pushTarget(this) // ! value = this.getter.call(this.vm, this.vm) popTarget() this.cleanupDeps() return value
先看這裏對 getter
的調用,返回到 ._mount()
中,能夠看到,是調用了 vm._update(vm._render(), hydrating)
,涉及兩個方法:
vm._render():返回虛擬節點(VNode)
vm._update()
來看 _update()
的邏輯,這裏應該是進行 Virtual DOM 的更新:
// 參數:vnode, hydrating vm = this prevEl = vm.$el prevVnode = vm._vnode prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode if (!prevVnode) { // 初次加載 vm.$el = vm.__patch__(vm.$el, vnode, ...) } else { // 更新 vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // 後續屬性配置,略....
參考 Virtual DOM 的通常邏輯,這裏是差很少的處理過程,再也不贅述。
綜上,這裏的 watcher 主要做用應該是在數據發生變動時,觸發從新渲染和更新視圖的處理:vm._update(vm._render())
。
接下來,咱們看下 watcher 是如何發揮做用的,參考 Vue 1.0 的經驗,下面應該是關於依賴收集、數據綁定方面的細節了,而這一部分,和 Vue 1.0 差異不大。
watcher.get()
中調用的 pushTarget()
和 popTarget()
來自文件:src/core/observer/dep.js
。
pushTarget()
和 popTarget()
兩個方法,用於處理 Dep.target
,顯然 Dep.target
在 wather.getter
的調用過程當中會用到,調用時會涉及到依賴收集,從而創建起數據綁定的關係。
在 Dep
類的 .dep()
方法中用到了 Dep.target
,調用方式爲:
Dep.target.addDep(this)
能夠想見,在使用數據進行渲染的過程當中,會對數據屬性進行「讀」操做,從而觸發 dep.depend()
,進而收集到這個依賴關係。下面來找一下這樣的調用的位置。
在 state.js
中找到一處,makeComputedGetter()
函數中經過 watcher.depend()
間接調用了 dep.depend()
。不過 computedGetter 應該不是最主要的地方,根據 Vue 1.0 的經驗,仍是要找對數據進行「數據劫持」的地方,應該是defineReactive()
。
defineReactive()
定義在文件 src/core/observer/index.js
。
// 參數:obj, key, val, customSetter? dep = new Dep() childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { // 略,調用了 dep.depend() }, set: function () { // 略,調用 dep.notify() } })
結合 Vue 1.0 經驗,這裏應該就是數據劫持的關鍵了。數據原有的屬性被從新定義,屬性的 get()
被調用時,會經過 dep.depend()
收集依賴關係,記錄到 vm 中;而在 set()
被調用時,則會判斷屬性值是否發生變動,若是發生變動,則經過 dep.notify()
來通知 vm,從而觸發 vm 的更新操做,實現 UI 與數據的同步,這也就是數據綁定後的效果了。
回過頭來看 state.js
,是在 initProps()
中調用了 defineReactive()
。而 initProps()
在 initState()
中調用,後者則是在 Vue.prototype._init()
中被調用。
不過最經常使用的實際上是在 initData()
中,對初始傳入的 data
進行劫持,不過裏面的過程稍微繞一些,是將這裏的 data 賦值到 vm._data
而且代理到了 vm
上,進一步的處理還涉及 observe()
和 Observer
類。這裏不展開了。
綜上,數據綁定的實現過程爲:
初始化:new Vue() -> vm._init()
數據劫持:initState(vm) -> initProps(), initData() -> dep.depend()
依賴收集:vm.$mount() -> vm._mount() -> new Watcher() -> vm._render()
首先來看 initRender()
,這裏在 vm 上初始化了兩個與建立虛擬元素相關的方法:
vm._c()
vm.$createElement()
其內部實現都是調用 createElement()
,來自文件:src/core/vdom/create-element.js
。
而在 renderMixin()
中初始化了 Vue.prototype._render()
方法,其中建立 vnode 的邏輯爲:
render = vm.$options.render try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // ... }
這裏傳入 render()
是一個會返回 vnode 的函數。
接下來看 vm._update()
的邏輯,這部分在前面有介紹,初次渲染時是經過調用 vm.__patch__()
來實現。那麼 vm.__patch__()
是在哪裏實現的呢?在 _update()
代碼中有句註釋,提到:
// Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used.
在文件 web-runtime.js
中,找到了:
Vue.prototype.__patch__ = inBrowser ? patch : noop
顯然示在瀏覽器環境下使用 patch()
,來自:src/platforms/web/runtime/patch.js
,其實現是經過 createPatchFunction()
,來自文件 src/core/vdom/patch
。
OK,以上線索都指向了 vdom 相關的模塊,也就是說,顯然是 vdom 也就是 Virtual DOM 參與了渲染和更新。
不過還有個問題沒有解決,那就是原始的字符串模塊,是如何轉成用於 Virtual DOM 建立的函數調用的呢?這裏會有一個解析的過程。
回到入口文件 web-runtime-with-compiler.js
,在 Vue.prototype.$mount()
中,有一個關鍵的調用:compileToFunctions(template, ...)
,template
變量值爲傳入的參數解析獲得的模板內容。
文件:src/platforms/web/compiler/index.js
函數 compileToFunctions()
的基本邏輯:
// 參數:template, options?, vm? res = {} compiled = compile(template, options) res.render = makeFunction(compiled.render) // 拷貝數組元素: // res.staticRenderFns <= compiled.staticRenderFns return res
這裏對模板進行了編譯(compile()
),最終返回了根據編譯結果獲得的 render()、staticRenderFns
。再看 web-runtime-with-compiler.js
中 Vue.prototype.$mount()
的邏輯,則是將這裏獲得的結果寫入了 vm.$options
中,也就是說,後面 vm._render()
中會使用這裏的 render()
。
再來看 compile()
函數,這裏是實現模板解析的核心,來作文件 src/compiler/index.js
,基本邏輯爲:
// 參數:template, options ast = parse(template.trim(), options) optimize(ast, options) code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns }
邏輯很清晰,首先從模板進行解析獲得抽象語法樹(ast),進行優化,最後生成結果代碼。整個過程當中確定會涉及到 Vue 的語法,包括指令、組件嵌套等等,不只僅是獲得構建 Virtual DOM 的代碼。
須要注意的是,編譯獲得 render 實際上是代碼文本,經過 new Function(code)
的方式轉爲函數。
Vue2 相比 Vue1 一個主要的區別在於引入了 Virtual DOM,但其 MVVM 的特性還在,也就是說仍有一套數據綁定的機制。
此外,Virtual DOM 的存在,使得原有的視圖模板須要轉變爲函數調用的模式,從而在每次有更新時能夠從新調用獲得新的 vnode,從而應用 Virtual DOM 的更新機制。爲此,Vue2 實現了編譯器(compiler),這也意味着 Vue2 的模板能夠是純文本,而沒必要是 DOM 元素。
Vue2 基本運行機制總結爲:
文本模板,編譯獲得生成 vnode 的函數(render),該過程當中會識別並記錄 Vue 的指令和其餘語法
new Vue() 獲得 vm 對象,其中傳入的數據會進行數據劫持處理,從而能夠收集依賴,實現數據綁定
渲染過程是將全部數據交由渲染函數(render)進行調用獲得 vnode,應該 Virtual DOM 的機制實現初始渲染和更新
對 Vue2 的源碼分析,是基於我以前對 Vue1 的分析和對 Virtual DOM 的瞭解,見【連接】中以前的文章。
水平有限,錯漏不免,歡迎指正。
感謝閱讀!