源碼版本:v2.1.10
分析目標(biāo)
通過(guò)閱讀源碼,對(duì) Vue2 的基礎(chǔ)運(yùn)行機(jī)制有所了解赢底,主要是:
- Vue2 中數(shù)據(jù)綁定的實(shí)現(xiàn)方式
- Vue2 中對(duì) Virtual DOM 機(jī)制的使用方式
源碼初見(jiàn)
項(xiàng)目構(gòu)建配置文件為 build/config.js
砌溺,定位 vue.js 對(duì)應(yīng)的入口文件為 src/entries/web-runtime-with-compiler.js
,基于 rollup 進(jìn)行模塊打包。
代碼中使用 flow 進(jìn)行接口類型標(biāo)記和檢查闷旧,在打包過(guò)程中移除這些標(biāo)記蓉冈。為了閱讀代碼方便城舞,在 VS Code 中安裝了插件 Flow Language Support轩触,然后關(guān)閉工作區(qū) JS 代碼檢查,這樣界面就清爽很多了家夺。
Vue 應(yīng)用啟動(dòng)一般是通過(guò) new Vue({...})
脱柱,所以,先從該構(gòu)造函數(shù)著手拉馋。
注:本文只關(guān)注 Vue 在瀏覽器端的應(yīng)用榨为,不涉及服務(wù)器端代碼。
Vue 構(gòu)造函數(shù)
文件:src/core/instance/index.js
該文件只是構(gòu)造函數(shù)煌茴,Vue 原型對(duì)象的聲明分散在當(dāng)前目錄的多個(gè)文件中:
- init.js:
._init()
- state.js:
.$data
.$set()
.$delete()
.$watch()
- render.js:
._render()
... - events.js:
.$on()
.$once()
.$off()
.$emit()
- lifecycle.js:
._mount()
._update()
.$forceUpdate()
.$destroy()
構(gòu)造函數(shù)接收參數(shù) options
随闺,然后調(diào)用 this._init(options)
。
._init()
中進(jìn)行初始化蔓腐,其中會(huì)依次調(diào)用 lifecycle矩乐、events、render回论、state 模塊中的初始化函數(shù)散罕。
Vue2 中應(yīng)該是為了代碼更易管理,Vue 類的定義分散到了上面的多個(gè)文件中透葛。
其中笨使,對(duì)于 Vue.prototype
對(duì)象的定義,通過(guò) mixin 的方式在入口文件 core/index.js
中依次調(diào)用僚害。對(duì)于實(shí)例對(duì)象(代碼中通常稱為 vm
)則通過(guò) init 函數(shù)在 vm._init()
中依次調(diào)用硫椰。
Vue 公共接口
文件:src/core/index.js
這里調(diào)用了 initGlobalAPI()
來(lái)初始化 Vue 的公共接口,包括:
- Vue.util
- Vue.set
- Vue.delete
- Vue.nextTick
- Vue.options
- Vue.use
- Vue.mixin
- Vue.extend
- asset相關(guān)接口:配置在
src/core/config.js
中
Vue 啟動(dòng)過(guò)程
調(diào)用 new Vue({...})
后萨蚕,在內(nèi)部的 ._init()
的最后靶草,是調(diào)用 .$mount()
方法來(lái)“啟動(dòng)”。
在 web-runtime-with-compiler.js
和 web-runtime.js
中岳遥,定義了 Vue.prototype.$mount()
奕翔。不過(guò)兩個(gè)文件中的 $mount()
最終調(diào)用的是 ._mount()
內(nèi)部方法,定義在文件 src/core/instance/lifecycle.js
中浩蓉。
Vue.prototype._mount(el, hydrating)
簡(jiǎn)化邏輯后的偽代碼:
vm = this
vm._watcher = new Watcher(vm, updateComponent)
接下來(lái)看 Watcher
派继。
Watcher
文件:src/core/observer/watcher.js
先看構(gòu)造函數(shù)的簡(jiǎn)化邏輯:
// 參數(shù):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
先看這里對(duì) getter
的調(diào)用,返回到 ._mount()
中认轨,可以看到绅络,是調(diào)用了 vm._update(vm._render(), hydrating)
,涉及兩個(gè)方法:
- vm._render():返回虛擬節(jié)點(diǎn)(VNode)
- vm._update()
來(lái)看 _update()
的邏輯,這里應(yīng)該是進(jìn)行 Virtual DOM 的更新:
// 參數(shù):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
// 后續(xù)屬性配置恩急,略....
參考 Virtual DOM 的一般邏輯杉畜,這里是差不多的處理過(guò)程,不再贅述衷恭。
綜上此叠,這里的 watcher 主要作用應(yīng)該是在數(shù)據(jù)發(fā)生變更時(shí),觸發(fā)重新渲染和更新視圖的處理:vm._update(vm._render())
匾荆。
接下來(lái)拌蜘,我們看下 watcher 是如何發(fā)揮作用的杆烁,參考 Vue 1.0 的經(jīng)驗(yàn)牙丽,下面應(yīng)該是關(guān)于依賴收集、數(shù)據(jù)綁定方面的細(xì)節(jié)了兔魂,而這一部分烤芦,和 Vue 1.0 差別不大。
數(shù)據(jù)綁定
watcher.get()
中調(diào)用的 pushTarget()
和 popTarget()
來(lái)自文件:src/core/observer/dep.js
析校。
pushTarget()
和 popTarget()
兩個(gè)方法构罗,用于處理 Dep.target
,顯然 Dep.target
在 wather.getter
的調(diào)用過(guò)程中會(huì)用到智玻,調(diào)用時(shí)會(huì)涉及到依賴收集遂唧,從而建立起數(shù)據(jù)綁定的關(guān)系。
在 Dep
類的 .dep()
方法中用到了 Dep.target
吊奢,調(diào)用方式為:
Dep.target.addDep(this)
可以想見(jiàn)盖彭,在使用數(shù)據(jù)進(jìn)行渲染的過(guò)程中,會(huì)對(duì)數(shù)據(jù)屬性進(jìn)行“讀”操作页滚,從而觸發(fā) dep.depend()
召边,進(jìn)而收集到這個(gè)依賴關(guān)系。下面來(lái)找一下這樣的調(diào)用的位置裹驰。
在 state.js
中找到一處隧熙,makeComputedGetter()
函數(shù)中通過(guò) watcher.depend()
間接調(diào)用了 dep.depend()
。不過(guò) computedGetter 應(yīng)該不是最主要的地方幻林,根據(jù) Vue 1.0 的經(jīng)驗(yàn)贞盯,還是要找對(duì)數(shù)據(jù)進(jìn)行“數(shù)據(jù)劫持”的地方,應(yīng)該是defineReactive()
沪饺。
defineReactive()
定義在文件 src/core/observer/index.js
躏敢。
// 參數(shù):obj, key, val, customSetter?
dep = new Dep()
childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
// 略,調(diào)用了 dep.depend()
},
set: function () {
// 略随闽,調(diào)用 dep.notify()
}
})
結(jié)合 Vue 1.0 經(jīng)驗(yàn)父丰,這里應(yīng)該就是數(shù)據(jù)劫持的關(guān)鍵了。數(shù)據(jù)原有的屬性被重新定義,屬性的 get()
被調(diào)用時(shí)蛾扇,會(huì)通過(guò) dep.depend()
收集依賴關(guān)系攘烛,記錄到 vm 中;而在 set()
被調(diào)用時(shí)镀首,則會(huì)判斷屬性值是否發(fā)生變更坟漱,如果發(fā)生變更,則通過(guò) dep.notify()
來(lái)通知 vm更哄,從而觸發(fā) vm 的更新操作芋齿,實(shí)現(xiàn) UI 與數(shù)據(jù)的同步,這也就是數(shù)據(jù)綁定后的效果了成翩。
回過(guò)頭來(lái)看 state.js
觅捆,是在 initProps()
中調(diào)用了 defineReactive()
。而 initProps()
在 initState()
中調(diào)用麻敌,后者則是在 Vue.prototype._init()
中被調(diào)用栅炒。
不過(guò)最常用的其實(shí)是在 initData()
中,對(duì)初始傳入的 data
進(jìn)行劫持术羔,不過(guò)里面的過(guò)程稍微繞一些赢赊,是將這里的 data 賦值到 vm._data
并且代理到了 vm
上,進(jìn)一步的處理還涉及 observe()
和 Observer
類级历。這里不展開(kāi)了释移。
綜上,數(shù)據(jù)綁定的實(shí)現(xiàn)過(guò)程為:
- 初始化:new Vue() -> vm._init()
- 數(shù)據(jù)劫持:initState(vm) -> initProps(), initData() -> dep.depend()
- 依賴收集:vm.$mount() -> vm._mount() -> new Watcher() -> vm._render()
渲染
首先來(lái)看 initRender()
寥殖,這里在 vm 上初始化了兩個(gè)與創(chuàng)建虛擬元素相關(guān)的方法:
- vm._c()
- vm.$createElement()
其內(nèi)部實(shí)現(xiàn)都是調(diào)用 createElement()
玩讳,來(lái)自文件:src/core/vdom/create-element.js
。
而在 renderMixin()
中初始化了 Vue.prototype._render()
方法扛禽,其中創(chuàng)建 vnode 的邏輯為:
render = vm.$options.render
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
}
這里傳入 render()
是一個(gè)會(huì)返回 vnode 的函數(shù)锋边。
接下來(lái)看 vm._update()
的邏輯,這部分在前面有介紹编曼,初次渲染時(shí)是通過(guò)調(diào)用 vm.__patch__()
來(lái)實(shí)現(xiàn)豆巨。那么 vm.__patch__()
是在哪里實(shí)現(xiàn)的呢?在 _update()
代碼中有句注釋掐场,提到:
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
在文件 web-runtime.js
中往扔,找到了:
Vue.prototype.__patch__ = inBrowser ? patch : noop
顯然示在瀏覽器環(huán)境下使用 patch()
,來(lái)自:src/platforms/web/runtime/patch.js
熊户,其實(shí)現(xiàn)是通過(guò) createPatchFunction()
萍膛,來(lái)自文件 src/core/vdom/patch
。
OK嚷堡,以上線索都指向了 vdom 相關(guān)的模塊蝗罗,也就是說(shuō)艇棕,顯然是 vdom 也就是 Virtual DOM 參與了渲染和更新。
不過(guò)還有個(gè)問(wèn)題沒(méi)有解決串塑,那就是原始的字符串模塊沼琉,是如何轉(zhuǎn)成用于 Virtual DOM 創(chuàng)建的函數(shù)調(diào)用的呢?這里會(huì)有一個(gè)解析的過(guò)程桩匪。
回到入口文件 web-runtime-with-compiler.js
打瘪,在 Vue.prototype.$mount()
中,有一個(gè)關(guān)鍵的調(diào)用:compileToFunctions(template, ...)
傻昙,template
變量值為傳入的參數(shù)解析得到的模板內(nèi)容闺骚。
模板解析
文件:src/platforms/web/compiler/index.js
函數(shù) compileToFunctions()
的基本邏輯:
// 參數(shù):template, options?, vm?
res = {}
compiled = compile(template, options)
res.render = makeFunction(compiled.render)
// 拷貝數(shù)組元素:
// res.staticRenderFns <= compiled.staticRenderFns
return res
這里對(duì)模板進(jìn)行了編譯(compile()
),最終返回了根據(jù)編譯結(jié)果得到的 render()妆档、staticRenderFns
僻爽。再看 web-runtime-with-compiler.js
中 Vue.prototype.$mount()
的邏輯,則是將這里得到的結(jié)果寫入了 vm.$options
中过吻,也就是說(shuō)进泼,后面 vm._render()
中會(huì)使用這里的 render()
。
再來(lái)看 compile()
函數(shù)纤虽,這里是實(shí)現(xiàn)模板解析的核心,來(lái)做文件 src/compiler/index.js
绞惦,基本邏輯為:
// 參數(shù):template, options
ast = parse(template.trim(), options)
optimize(ast, options)
code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
邏輯很清晰逼纸,首先從模板進(jìn)行解析得到抽象語(yǔ)法樹(shù)(ast),進(jìn)行優(yōu)化济蝉,最后生成結(jié)果代碼杰刽。整個(gè)過(guò)程中肯定會(huì)涉及到 Vue 的語(yǔ)法,包括指令王滤、組件嵌套等等贺嫂,不僅僅是得到構(gòu)建 Virtual DOM 的代碼。
需要注意的是雁乡,編譯得到 render 其實(shí)是代碼文本第喳,通過(guò) new Function(code)
的方式轉(zhuǎn)為函數(shù)。
總結(jié)
Vue2 相比 Vue1 一個(gè)主要的區(qū)別在于引入了 Virtual DOM踱稍,但其 MVVM 的特性還在曲饱,也就是說(shuō)仍有一套數(shù)據(jù)綁定的機(jī)制。
此外珠月,Virtual DOM 的存在扩淀,使得原有的視圖模板需要轉(zhuǎn)變?yōu)楹瘮?shù)調(diào)用的模式,從而在每次有更新時(shí)可以重新調(diào)用得到新的 vnode啤挎,從而應(yīng)用 Virtual DOM 的更新機(jī)制驻谆。為此,Vue2 實(shí)現(xiàn)了編譯器(compiler),這也意味著 Vue2 的模板可以是純文本胜臊,而不必是 DOM 元素氛谜。
Vue2 基本運(yùn)行機(jī)制總結(jié)為:
- 文本模板,編譯得到生成 vnode 的函數(shù)(render)区端,該過(guò)程中會(huì)識(shí)別并記錄 Vue 的指令和其他語(yǔ)法
- new Vue() 得到 vm 對(duì)象值漫,其中傳入的數(shù)據(jù)會(huì)進(jìn)行數(shù)據(jù)劫持處理,從而可以收集依賴织盼,實(shí)現(xiàn)數(shù)據(jù)綁定
- 渲染過(guò)程是將所有數(shù)據(jù)交由渲染函數(shù)(render)進(jìn)行調(diào)用得到 vnode杨何,應(yīng)該 Virtual DOM 的機(jī)制實(shí)現(xiàn)初始渲染和更新
寫在最后
對(duì) Vue2 的源碼分析,是基于我之前對(duì) Vue1 的分析和對(duì) Virtual DOM 的了解沥邻,見(jiàn)【鏈接】中之前的文章危虱。
水平有限,錯(cuò)漏難免,歡迎指正。
感謝閱讀荒辕!