本文 Vue 源碼版本:2.6.11,為了便于理解,均有所刪減。
本文將從以下兩個(gè)方面進(jìn)行探索:
從 Vue 初始化,到首次渲染生成 DOM 的流程。
從 Vue 數(shù)據(jù)修改,到頁(yè)面更新 DOM 的流程。
先從最簡(jiǎn)單的一段 Vue 代碼開始:
<template>
<div>
{{ message }}
</div>
</template>
<script>
new Vue({
data() {
return {
message: 'hello world',
};
},
});
</script>
這段代碼很簡(jiǎn)單,最終會(huì)在頁(yè)面上打印一個(gè) hello world,它是如何實(shí)現(xiàn)的呢?
我們從源頭:new Vue 的地方開始分析。
// 執(zhí)行 new Vue 時(shí)會(huì)依次執(zhí)行以下方法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)
// 5. 調(diào)用 walk 方法,遍歷 data 中的每一個(gè)屬性,監(jiān)聽數(shù)據(jù)的變化。
function walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
// 6. 執(zhí)行 defineProperty 監(jiān)聽數(shù)據(jù)讀取和設(shè)置。
function defineReactive(obj, key, val) {
// 為每個(gè)屬性創(chuàng)建 Dep(依賴搜集的容器,后文會(huì)講)
const dep = new Dep();
// 綁定 get、set
Object.defineProperty(obj, key, {
get() {
const value = val;
// 如果有 target 標(biāo)識(shí),則進(jìn)行依賴搜集
if (Dep.target) {
dep.depend();
}
return value;
},
set(newVal) {
val = newVal;
// 修改數(shù)據(jù)時(shí),通知頁(yè)面重新渲染
dep.notify();
},
});
}
數(shù)據(jù)描述符綁定完成后,我們就能得到以下的流程圖:
圖中我們可以看到,Vue 初始化時(shí),進(jìn)行了數(shù)據(jù)的 get、set 綁定,并創(chuàng)建了一個(gè) Dep 對(duì)象。
對(duì)于數(shù)據(jù)的 get、set 綁定我們并不陌生,但是 Dep 對(duì)象什么呢?
Dep 對(duì)象用于依賴收集,它實(shí)現(xiàn)了一個(gè)發(fā)布訂閱模式,完成了數(shù)據(jù) Data 和渲染視圖 Watcher 的訂閱,我們一起來剖析一下。
class Dep {
// 根據(jù) ts 類型提示,我們可以得出 Dep.target 是一個(gè) Watcher 類型。
static target: ?Watcher;
// subs 存放搜集到的 Watcher 對(duì)象集合
subs: Array<Watcher>;
constructor() {
this.subs = [];
}
addSub(sub: Watcher) {
// 搜集所有使用到這個(gè) data 的 Watcher 對(duì)象。
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 搜集依賴,最終會(huì)調(diào)用上面的 addSub 方法
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 調(diào)用對(duì)應(yīng)的 Watcher,更新視圖
subs[i].update();
}
}
}
根據(jù)對(duì) Dep 的源碼分析,我們得到了下面這張邏輯圖:
了解 Data 和 Dep 之后,我們來繼續(xù)揭開 Watcher 的面紗。
class Watcher {
constructor(vm: Component, expOrFn: string | Function) {
// 將 vm._render 方法賦值給 getter。
// 這里的 expOrFn 其實(shí)就是 vm._render,后文會(huì)講到。
this.getter = expOrFn;
this.value = this.get();
}
get() {
// 給 Dep.target 賦值為當(dāng)前 Watcher 對(duì)象
Dep.target = this;
// this.getter 其實(shí)就是 vm._render
// vm._render 用來生成虛擬 dom、執(zhí)行 dom-diff、更新真實(shí) dom。
const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep: Dep) {
// 將當(dāng)前的 Watcher 添加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 開啟異步隊(duì)列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 和初始化一樣,會(huì)調(diào)用 get 方法,更新視圖
const value = this.get();
}
}
源碼中我們看到,Watcher 實(shí)現(xiàn)了渲染方法 _render
和 Dep 的關(guān)聯(lián), 初始化 Watcher 的時(shí)候,打上 Dep.target 標(biāo)識(shí),然后調(diào)用 get 方法進(jìn)行頁(yè)面渲染。加上上文的 Data,目前 Data、Dep、Watcher 三者的關(guān)系如下:
我們?cè)倮ù幌抡麄€(gè)流程:Vue 通過 defineProperty
完成了 Data 中所有數(shù)據(jù)的代理,當(dāng)數(shù)據(jù)觸發(fā) get 查詢時(shí),會(huì)將當(dāng)前的 Watcher 對(duì)象加入到依賴收集池 Dep 中,當(dāng)數(shù)據(jù) Data 變化時(shí),會(huì)觸發(fā) set 通知所有使用到這個(gè) Data 的 Watcher 對(duì)象去 update 視圖。
目前的整體流程如下:
上圖的流程中 Data 和 Dep 都是 Vue 初始化時(shí)創(chuàng)建的,但現(xiàn)在我們并不知道 Wacher 是從哪里創(chuàng)建的,帶著這個(gè)問題,我們接著往下探索。
上文中,我們分析了初始化 Vue 過程中處理數(shù)據(jù)的部分,接下來,我們分析一下數(shù)據(jù)渲染的部分。
其實(shí) new Vue 執(zhí)行到最后,會(huì)調(diào)用 mount 方法,將 Vue 實(shí)例渲染成 dom 。
// new Vue 執(zhí)行流程。
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,編譯 Vue 中的 template 模板,生成 render 方法。
// 4. Vue.prototype.$mount 調(diào)用上面的 render 方法掛載 dom。
// 5. mountComponent
// 6. 創(chuàng)建 Watcher 實(shí)例
const updateComponent = () => {
vm._update(vm._render());
};
// 結(jié)合上文,我們就能得出,updateComponent 就是傳入 Watcher 內(nèi)部的 getter 方法。
new Watcher(vm, updateComponent);
// 7. new Watcher 會(huì)執(zhí)行 Watcher.get 方法
// 8. Watcher.get 會(huì)執(zhí)行 this.getter.call(vm, vm) ,也就是執(zhí)行 updateComponent 方法
// 9. updateComponent 會(huì)執(zhí)行 vm._update(vm._render())
// 10. 調(diào)用 vm._render 生成虛擬 dom
Vue.prototype._render = function (): VNode {
const vm: Component = this;
const { render } = vm.$options;
let vnode = render.call(vm._renderProxy, vm.$createElement);
return vnode;
};
// 11. 調(diào)用 vm._update(vnode) 渲染虛擬 dom
Vue.prototype._update = function (vnode: VNode) {
const vm: Component = this;
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
// 12. vm.__patch__ 方法就是做的 dom diff 比較,然后更新 dom,這里就不展開了。
看完 Vue 模板渲染的過程,我們可以得到如下的流程圖:
到這里,我們就知道了 Watcher 其實(shí)是在 Vue 初始化的階段創(chuàng)建的,屬于生命周期中 beforeMount 的位置創(chuàng)建的,創(chuàng)建 Watcher 時(shí)會(huì)執(zhí)行 render 方法,最終將 Vue 代碼渲染成真實(shí)的 DOM。
我們?cè)賹⒅暗牧鞒陶弦幌拢湍艿玫揭韵碌牧鞒蹋?/p>
上圖分析了 Vue 初始化到渲染 DOM 的整個(gè)過程,最后我們?cè)俜治鲆幌拢?dāng)數(shù)據(jù)變化時(shí),Vue 又是怎么進(jìn)行更新的?
其實(shí),在上圖也能看出,在 Data 變化時(shí),會(huì)調(diào)用 Dep.notify 方法,隨即調(diào)用 Watcher 內(nèi)部的 update 方法,此方法會(huì)將所有使用到這個(gè) Data 的 Watcher 加入一個(gè)隊(duì)列,并開啟一個(gè)異步隊(duì)列進(jìn)行更新,最終執(zhí)行 _render
方法完成頁(yè)面更新。
整體的流程如下:
好了,探索到這里,Vue 的響應(yīng)式原理,已經(jīng)被我們分析透徹了,如果你還沒有明白,不妨再細(xì)品一下上圖。
本來探索到上面的流程圖就結(jié)束了,但好奇的我又想到了一個(gè)問題 ??。
Vue 組件又是怎么渲染的呢?
帶著這個(gè)問題,我繼續(xù)查閱了源碼。
// 從模板編譯開始,當(dāng)發(fā)現(xiàn)一個(gè)自定義組件時(shí),會(huì)執(zhí)行以下函數(shù)
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement
// 6. createComponent
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// $options._base 其實(shí)就是全局 Vue 構(gòu)造函數(shù),在初始化時(shí) initGlobalAPI 中定義的:Vue.options._base = Vue
const baseCtor = context.$options._base;
// Ctor 就是 Vue 組件中 <script> 標(biāo)簽下 export 出的對(duì)象
if (isObject(Ctor)) {
// 將組件中 export 出的對(duì)象,繼承自 Vue,得到一個(gè)構(gòu)造函數(shù)
// 相當(dāng)于 Vue.extend(YourComponent)
Ctor = baseCtor.extend(Ctor);
}
const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
return vnode;
}
// 7. 實(shí)現(xiàn)組件繼承 Vue,并調(diào)用 Vue._init 方法,進(jìn)行初始化
Vue.extend = function (extendOptions: Object): Function {
const Super = this;
const Sub = function VueComponent(options) {
// 調(diào)用 Vue.prototype._init,之后的流程就和首次加載保持一致
this._init(options);
};
// 原型繼承,相當(dāng)于:Component extends Vue
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
return Sub;
};
看完組件渲染的源碼后,結(jié)合上文,重新整理了一張流程圖,圖中的藍(lán)色部分就是渲染組件的過程。
好了,現(xiàn)在是真的結(jié)束了,最終的流程圖就是上面的這一張圖。
問個(gè)問題,現(xiàn)在你理解 Vue 響應(yīng)式原理了嗎?
如果仍覺得不好理解,我這里還準(zhǔn)備了一張帶標(biāo)注的簡(jiǎn)圖 ??
本文從源碼的角度,介紹了 Vue 響應(yīng)式原理,來簡(jiǎn)單回顧一下吧。
聯(lián)系客服