響應式原理
源碼目錄:https://github.com/vuejs/vue-next/tree/master/packages/reactivity
模塊
ref:
reactive:
computed:
effect:
operations:提供TrackOpTypes和TriggerOpTypes兩個枚舉類型,供其他模塊使用
剖析
Vue2響應式原理
什么是響應式數據?即A依賴於B數據,當B值發生變化時,通知A。很顯然,這里應該使用觀察者模式
在vue2中的響應式原理:剖析Vue原理&實現雙向綁定MVVM
上面的文章將整個Vue的大致實現都分析了,就響應式這塊來說,大概的邏輯是這幾個模塊Observer,Watcher,Dep。
Observer負責通過defineProperty劫持數據Data,每個被劫持的Data都各自在閉包中維護一個Dep的實例,用於收集依賴着它的Watcher【即觀察者】(都實現了一個update方法),被收集的Watcher存入Dep實例的subs數組中。如果Data是對象,則遞歸搜集。
Dep維護一個公共的Target屬性,在觸發劫持前,將Target設置為當前Watcher, 然后觸發getter將Target(Watcher)收集到subs中。然后再將Target置為null
Data數據變更的時候觸發setter,然后從Data維護的Dep實例的subs數組中將Watcher取出來一一執行其update方法。如果變更的值是對象,再劫持之。
用一個最簡單的偽代碼來說明(省略掉了對值是復雜數據的處理,原理是一樣的)
// Vue2響應式原理的基本使用(偽代碼)
data = { age: 10 };
new Observer(data) // 數據劫持,黑色箭頭
new Wachter(target, 'age', function update() { ... }) // 添加觀察者,綠色箭頭
data.age = 20 // 被觀察者變更,通知觀察者, 紅色箭頭
對應的數據流程如下
就上面的過程,實際上還是有比較大的問題
1.如果Watcher使用的Data是對象類型,那么Data中所有的子屬性都需要遞歸將Watcher收集,這是個資源浪費。
2.數據劫持和依賴收集是強耦合關系
3.對數組的劫持也沒有做好,部分操作不是響應式的。
effect.ts
為了解決vue2的問題,依賴收集(即添加觀察者/通知觀察者)模塊單獨出來,就是現在的effect
用來生成/處理/追蹤reactiveEffect數據,主要是收集數據依賴(觀察者),通知收集的依賴(觀察者)。
提供了三個函數主要函數:effect/track/trigger。
effect是將傳入的函數轉化為reactiveEffect格式的函數
track主要功能是將reactiveEffect添加為target[key]的觀察者
trigger主要功能是通知target[key]的觀察者(將觀察者隊列函數一一取出來執行)
effect(fn, options):ReactiveEffect
返回一個effect數據:reactiveEffect函數。
執行reactiveEffect即可將數據加入可追蹤隊列effectStack,並將當前數據設置為activeEffect,並執行fn,fn執行完畢之后恢復activeEffect。
【注意】:必須要在fn函數中執行track才能將reactiveEffect添加為target[key]的觀察者,因為track內部只會處理當前的activeEffect,activeEffect沒有值則直接返回
track(target, type, key)
將activeEffect添加為target[key]的觀察者,如果activeEffect無值,則直接返回。target[key]數據被緩存到targetMap中以{target-> key-> dep}格式存儲,優化內存開銷。
當前activeEffect(在調用reactiveEffect函數時會將reactiveEffect設置為activeEffect)添加為target[key]的觀察者,被添加到target[key]的觀察者隊列dep中【dep.add(activeEffect)】
當前target[key]的觀察者隊列dep也會被activeEffect收集【activeEffect.deps.push(dep)】
trigger(target, type, key, newValue, oldValue, oldTarget)
通知target[key]的觀察者,即target-> key-> dep中存放的數據,全部一一取出來執行
如果觀察者有提供scheduler則執行scheduler函數,否則執行觀察者(函數類型)本身
流程是:
首先要將某個函數fn包裹一層為reactiveEffect函數。
當執行reactiveEffect函數時內部會將當前reactiveEffect函數標記為activeEffect,然后執行fn。
fn內部可以調用track,將activeEffect添加為target[key]的觀察者,加入隊列dep中。當然activeEffect也收集了target[key]的觀察者隊列dep。
這時,如果修改target[key]的值,然后調用trigger,觸發通知target[key]的觀察者。trigger中會將對應的觀察者隊列中的觀察者一一取出執行。
import { effect, track, trigger } from 'vue'
let target = {
age: 10
}
const fn = () => {
// 將fn對應的reactiveEffect函數添加到target.age的觀察者隊列
track(target, 'get', 'age')
// 觸發target.age的trigger【通知觀察者】, 也會執行該函數
}
// 將fn函數包裹一層為reactiveEffect函數
const myEffect = effect(fn, { lazy: true })
// myEffect每次執行都會將自己設置為activeEffect,並執行fn函數
// fn內部會將對應的reactiveEffect函數添加到target.age的觀察者隊列
myEffect()
// 設置新值並手動通知target.age的所有觀察者
target.age = 20
// 通知target.age的觀察者
trigger(target, 'set', 'age')
結合流程說明看這段代碼,數據流圖
理論上來說,將reactiveEffect添加為target[key]的觀察者不一定要在fn中進行。但不這樣,用戶需要手動為target[key]指定觀察者,形如
activeEffect = reactiveEffect
track(target, 'get', 'age') // 內部會將activeEffect添加為target.age的觀察者
activeEffect = null
為了簡化處理,reactiveEffect內部處理為
// reactiveEffect 內部
try {
effectStack.push(effect)
// 當前effect設置為activeEffect
// 第一次track被調用時,該effect會被加入effectStack
activeEffect = effect
// 執行fn的過程中會對activeEffect做處理
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
在fn執行之前已經將reactiveEffect設置為activeEffect,並且fn執行完畢之后會恢復activeEffect,
這樣fn中只需要調用一下track,就將fn對應的reactiveEffect添加為target.age的觀察者了,代碼如下
// fn
const fn = () => {
...
track(target, 'get', 'age')
return get.age
}
我們將最開始的那個例子改造成一個更加真實的的例子
import { effect, track, trigger } from 'vue'
let target = {
_age: 10,
set age(val) {
this._age = val
trigger(this, 'set', 'age')
}
}
const watcher = () => {
console.log('target.age有更改,則通知我')
}
const fn = () => {
if(!target._isTracked){
target._isTracked = true
track(target, 'get', 'age')
console.log('添加fn的reactiveEffect函數添加到target.age的觀察者隊列')
}else{
watcher()
}
}
fn._isTracked = false
const myEffect = effect(fn, { lazy: true })
myEffect() //打印: '添加fn的reactiveEffect函數添加到target.age的觀察者隊列'
target.age = 20 //打印: '觸發target.age的trigger【通知觀察者】, 進入此處'