這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助


最近部門分享,有同學提到了 Vue 回應式原理,大家在討論時,發現一些同學對這一知識理解還不夠深入,不能形成一個倍訓,為了幫助大家理解這個問題,我重新過了一下 Vue 原始碼,并整理了多張流程圖,便于大家理解,
- Vue 初始化
- 模板渲染
- 組件渲染
本文 Vue 原始碼版本:2.6.11,為了便于理解,均有所刪減,
本文將從以下兩個方面進行探索:
-
從 Vue 初始化,到首次渲染生成 DOM 的流程,
-
從 Vue 資料修改,到頁面更新 DOM 的流程,
Vue 初始化
先從最簡單的一段 Vue 代碼開始:
<template>
<div>
{{ message }}
</div>
</template>
<script>
new Vue({
data() {
return {
message: "hello world",
};
},
});
</script>
這段代碼很簡單,最侄訓在頁面上列印一個 hello world,它是如何實作的呢?
我們從源頭:new Vue 的地方開始分析,
// 執行 new Vue 時會依次執行以下方法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)
// 5. 呼叫 walk 方法,遍歷 data 中的每一個屬性,監聽資料的變化,
function walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
// 6. 執行 defineProperty 監聽資料讀取和設定,
function defineReactive(obj, key, val) {
// 為每個屬性創建 Dep(依賴搜集的容器,后文會講)
const dep = new Dep();
// 系結 get、set
Object.defineProperty(obj, key, {
get() {
const value = https://www.cnblogs.com/smileZAZ/archive/2022/10/17/val;
// 如果有 target 標識,則進行依賴搜集
if (Dep.target) {
dep.depend();
}
return value;
},
set(newVal) {
val = newVal;
// 修改資料時,通知頁面重新渲染
dep.notify();
},
});
}
資料描述符系結完成后,我們就能得到以下的流程圖:
圖中我們可以看到,Vue 初始化時,進行了資料的 get、set 系結,并創建了一個 Dep 物件,
對于資料的 get、set 系結我們并不陌生,但是 Dep 物件什么呢?
Dep 物件用于依賴收集,它實作了一個發布訂閱模式,完成了資料 Data 和渲染視圖 Watcher 的訂閱,我們一起來剖析一下,
class Dep {
// 根據 ts 型別提示,我們可以得出 Dep.target 是一個 Watcher 型別,
static target: ?Watcher;
// subs 存放搜集到的 Watcher 物件集合
subs: Array<Watcher>;
constructor() {
this.subs = [];
}
addSub(sub: Watcher) {
// 搜集所有使用到這個 data 的 Watcher 物件,
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 搜集依賴,最侄訓呼叫上面的 addSub 方法
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 呼叫對應的 Watcher,更新視圖
subs[i].update();
}
}
}
根據對 Dep 的原始碼分析,我們得到了下面這張邏輯圖:

了解 Data 和 Dep 之后,我們來繼續揭開 Watcher 的面紗,
class Watcher {
constructor(vm: Component, expOrFn: string | Function) {
// 將 vm._render 方法賦值給 getter,
// 這里的 expOrFn 其實就是 vm._render,后文會講到,
this.getter = expOrFn;
this.value = https://www.cnblogs.com/smileZAZ/archive/2022/10/17/this.get();
}
get() {
// 給 Dep.target 賦值為當前 Watcher 物件
Dep.target = this;
// this.getter 其實就是 vm._render
// vm._render 用來生成虛擬 dom、執行 dom-diff、更新真實 dom,
const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep: Dep) {
// 將當前的 Watcher 添加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 開啟異步佇列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 和初始化一樣,會呼叫 get 方法,更新視圖
const value = this.get();
}
}
原始碼中我們看到,Watcher 實作了渲染方法 _render 和 Dep 的關聯, 初始化 Watcher 的時候,打上 Dep.target 標識,然后呼叫 get 方法進行頁面渲染,加上上文的 Data,目前 Data、Dep、Watcher 三者的關系如下:
我們再拉通串一下整個流程:Vue 通過 defineProperty 完成了 Data 中所有資料的代理,當資料觸發 get 查詢時,會將當前的 Watcher 物件加入到依賴收集池 Dep 中,當資料 Data 變化時,會觸發 set 通知所有使用到這個 Data 的 Watcher 物件去 update 視圖,
目前的整體流程如下:
上圖的流程中 Data 和 Dep 都是 Vue 初始化時創建的,但現在我們并不知道 Wacher 是從哪里創建的,帶著這個問題,我們接著往下探索,
模板渲染
上文中,我們分析了初始化 Vue 程序中處理資料的部分,接下來,我們分析一下資料渲染的部分,
其實 new Vue 執行到最后,會呼叫 mount 方法,將 Vue 實體渲染成 dom ,
// new Vue 執行流程,
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,編譯 Vue 中的 template 模板,生成 render 方法,
// 4. Vue.prototype.$mount 呼叫上面的 render 方法掛載 dom,
// 5. mountComponent
// 6. 創建 Watcher 實體
const updateComponent = () => {
vm._update(vm._render());
};
// 結合上文,我們就能得出,updateComponent 就是傳入 Watcher 內部的 getter 方法,
new Watcher(vm, updateComponent);
// 7. new Watcher 會執行 Watcher.get 方法
// 8. Watcher.get 會執行 this.getter.call(vm, vm) ,也就是執行 updateComponent 方法
// 9. updateComponent 會執行 vm._update(vm._render())
// 10. 呼叫 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. 呼叫 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 其實是在 Vue 初始化的階段創建的,屬于生命周期中 beforeMount 的位置創建的,創建 Watcher 時會執行 render 方法,最終將 Vue 代碼渲染成真實的 DOM,
我們再將之前的流程整合一下,就能得到以下的流程:

上圖分析了 Vue 初始化到渲染 DOM 的整個程序,最后我們再分析一下,當資料變化時,Vue 又是怎么進行更新的?
其實,在上圖也能看出,在 Data 變化時,會呼叫 Dep.notify 方法,隨即呼叫 Watcher 內部的 update 方法,此方法會將所有使用到這個 Data 的 Watcher 加入一個佇列,并開啟一個異步佇列進行更新,最終執行 _render 方法完成頁面更新,
整體的流程如下:

好了,探索到這里,Vue 的回應式原理,已經被我們分析透徹了,如果你還沒有明白,不妨再細品一下上圖,
組件渲染
本來探索到上面的流程圖就結束了,但好奇的我又想到了一個問題 ??,
Vue 組件又是怎么渲染的呢?
帶著這個問題,我繼續查閱了原始碼,

// 從模板編譯開始,當發現一個自定義組件時,會執行以下函式
// 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 其實就是全域 Vue 建構式,在初始化時 initGlobalAPI 中定義的:Vue.options._base = Vue
const baseCtor = context.$options._base;
// Ctor 就是 Vue 組件中 <script> 標簽下 export 出的物件
if (isObject(Ctor)) {
// 將組件中 export 出的物件,繼承自 Vue,得到一個建構式
// 相當于 Vue.extend(YourComponent)
Ctor = baseCtor.extend(Ctor);
}
const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
return vnode;
}
// 7. 實作組件繼承 Vue,并呼叫 Vue._init 方法,進行初始化
Vue.extend = function (extendOptions: Object): Function {
const Super = this;
const Sub = function VueComponent(options) {
// 呼叫 Vue.prototype._init,之后的流程就和首次加載保持一致
this._init(options);
};
// 原型繼承,相當于:Component extends Vue
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
return Sub;
};
看完組件渲染的原始碼后,結合上文,重新整理了一張流程圖,圖中的藍色部分就是渲染組件的程序,

好了,現在是真的結束了,最終的流程圖就是上面的這一張圖,
問個問題,現在你理解 Vue 回應式原理了嗎?
如果仍覺得不好理解,我這里還準備了一張帶標注的簡圖 ??

思考與總結
本文從原始碼的角度,介紹了 Vue 回應式原理,來簡單回顧一下吧,
- 從 new Vue 開始,首先通過 get、set 監聽 Data 中的資料變化,同時創建 Dep 用來搜集使用該 Data 的 Watcher,
- 編譯模板,創建 Watcher,并將 Dep.target 標識為當前 Watcher,
- 編譯模板時,如果使用到了 Data 中的資料,就會觸發 Data 的 get 方法,然后呼叫 Dep.addSub 將 Watcher 搜集起來,
- 資料更新時,會觸發 Data 的 set 方法,然后呼叫 Dep.notify 通知所有使用到該 Data 的 Watcher 去更新 DOM,
本文轉載于:
https://juejin.cn/post/6857669921166491662
如果對您有所幫助,歡迎您點個關注,我會定時更新技術檔案,大家一起討論學習,一起進步,


轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/516425.html
標籤:其他
上一篇:Vue筆記
