響應式的基本概念
響應式是指當數據發生變化時,系統會自動更新與數據相關的 DOM 結構。
在 Vue2 中,響應式系統的實現基于Object.defineProperty。然而,Object.defineProperty有一些局限,如:無法監聽數組的變化、需要遍歷對象的每個屬性進行監聽、性能開銷較大。
在 Vue3 中,響應式系統的實現基于 ES6 的Proxy對象。Proxy可以直接監聽對象和數組的變化,而無需對每個屬性進行監聽,從而大大提高性能。同時,Proxy也可以解決Object.defineProperty無法監聽數組的問題。
響應式的關鍵在于vue的依賴收集機制。
簡化模型
為了更直觀的理解vue依賴收集的模型,我們先來看一個“簡單”的功能描述:
已知watcher函數,調用了一些“外部函數”:
function watcher () { console.log('watcher start') 函數1(); 函數2(); console.log('watcher end') }
能否設計一個依賴收集系統,使這些“外部函數”運行時,watcher也會隨之運行?
關鍵:如何判斷函數間的調用關系?
看似有點難,實際一點也不簡單,我們需要知道函數間調用關系。我們先看個例子:
function A() { console.log('A') }
function B() { console.log('B') }
function C() { console.log('C') }
...
function watcher () {
console.log('watcher start!')
/* *這里調用了上面的某些函數* */
console.log('watcher end!')
}
/* *這里運行了某些函數* */
watcher();
- watcher start!
- A
- B
- wathcer end!
- C
從運行結果我們可以看出watcher內部一定調用了A、B函數:
為啥?js是單線程的。
C函數一定在watcher外面嗎?不一定。例如:
function watcher () { console.log('start') A() B() setTimeout(()=>{ C() }) console.log('end') } watcher();
C函數這種咋辦?不管!我們只管肯定沒問題的!
我們由此可以確定
函數watcher執行期間,凡是運行過的函數,一定是watcher內部調用過的函數
根據這個原理,我們設計依賴收集系統如下:
// 當前的監聽函數 let activeEffect = null // 副作用函數 function effect (watcher) { activeEffect = watcher // watcher執行的期間就是依賴收集的階段 watcher(true) activeEffect= null } // isTracking:是否是依賴收集階段 function A (isTracking = false) { if (isTracking) { // 依賴收集階段,effects就是A的監聽函數集合 A.effects = A.effects || new Set() A.effects.add(activeEffect) } else { // 依賴運行階段 console.log('A觸發了') A.effects.forEach(fn => fn(true)) } } function B (isTracking = false) { /*** 與A類似 ***/ }
測試一下效果


看起來達到了要求。
將上面代碼優化一下,最終如下:
let activeEffect = null;
function effect (watcher) {
activeEffect = watcher;
watcher(true);
activeEffect = null;
}
const bucket = new WeakMap();
function track (target) {
const effects = bucket.get(target) || new Set();
activeEffect && effects.add(activeEffect);
bucket.set(target, effects);
}
function trigger (target) {
bucket.get(target)?.forEach?.(fn => fn(true));
}
function A (isTracking = false) {
if (isTracking) {
track(A);
} else {
console.log('A觸發了')
trigger(A);
}
}
function B (isTracking = false) {
}
這里將之前 A.effects = A.effects || new Set();依賴收集流程提取成track函數,監聽函數的觸發流程抽離為trigger函數;這樣,我們實現了一個簡單的依賴收集系統。
Vue依賴收集模型
我們知道Vue3是通過Proxy實現的依賴收集流程,Proxy示例:

1. Proxy對象get監聽,set觸發
Vue3中,Proxy代理數據在被讀取時“依賴收集”,在被賦值時會“觸發依賴”;我們試一下上面完成的依賴收集系統,看下效果:
const data = {
value: 1,
}
const proxyData = new Proxy(data, {
get(target, key) {
track(target);
return target[key];
},
set(target, key, value) {
trigger(target);
target[key] = value;
}
})
測試一下
測試代碼如下:

終端運行結果:

看起來效果不錯!但是下面的例子里有問題:


一個無關的屬性key的賦值也會觸發監聽函數!這不是我們想要的。為了精確監聽,還需要細化依賴收集系統。
2. “key”級依賴
我們可以將對象的屬性作為基本單位進行依賴收集。改造如下:
// 依賴收集函數,這里精確到keyfunction track (target, key) { const effects = bucket.get(target) || new Map(); const keyMap = effects.get(key) || new Set(); effects.set(key, keyMap); bucket.set(target, effects); activeEffect && keyMap.add(activeEffect);}// 依賴觸發函數,這里精確到keyfunction trigger (target, key) { const effects = bucket.get(target); if (!effects) return; const keyMap = effects.get(key); if (!keyMap) return; keyMap.forEach(effect => effect());}
const data = { value: 1}const proxyData = new Proxy(data, { get(target, key) {
// 具體到key進行收集 track(target, key); return target[key] }, set(target, key, value) {
// 觸發到key trigger(target, key); target[key] = value }})
這里試一下效果


這樣就實現了精確到屬性的監聽系統。看到這里,似乎完成的很不錯了,但是看到下面的例子:

這里value屬性由false變為true后,屬性data的就已不再參與監聽函數內的邏輯了;監聽函數不應該再響應data屬性,但實際上并沒有。因為依賴關系已經固化,data屬性只要變化就一定會觸發監聽,不管是否真的需要:

3. 分支切換
為了優化這一點,應將依賴關系實時更新,將多余的監聽去除。為此,vue采取的策略是:
每次監聽函數運行前,都要將自己的依賴關系清除;然后在運行期間重建依賴關系。(版權歸掘金硬毛巾原作者所有,侵刪)
審核編輯:黃飛
-
函數
+關注
關注
3文章
4417瀏覽量
67499 -
DOM
+關注
關注
0文章
18瀏覽量
9854 -
監聽系統
+關注
關注
0文章
7瀏覽量
6507
原文標題:Vue3響應式系統原理
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
一文解析Vue代碼層面的優化
基于TypeScript實現Vue3.0指令組件拖拽
關于vue如何去水印的解決方法的介紹
關于React和Vue產生一定的認知
如何使用springboot+vue搭建個人網站3
搭建基于Vue3+Vite2+Arco+Typescript+Pinia后臺管理系統模板
使用Vue3時遇到的一些問題
一文看懂Vue3響應式系統原理
評論