響應式的基本概念
響應式是指當數據發生變化時,系統會自動更新與數據相關的 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文章
4117瀏覽量
61507 -
DOM
+關注
關注
0文章
16瀏覽量
9536 -
監聽系統
+關注
關注
0文章
7瀏覽量
6399
原文標題:Vue3響應式系統原理
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論