vue3剖析:響應式原理——effect
阿新 • • 發佈:2020-09-10
# 響應式原理
原始碼目錄: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](https://www.cnblogs.com/chuaWeb/articles/13554465.html)
上面的文章將整個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 // 被觀察者變更,通知觀察者, 紅色箭頭
```
對應的資料流程如下
![Vue2響應式原理的基本使用流程](https://img2020.cnblogs.com/blog/831429/202009/831429-20200910180025951-1785345503.png)
就上面的過程,實際上還是有比較大的問題
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')
```
結合流程說明看這段程式碼,資料流圖
![effect簡單例子的資料流圖](https://img2020.cnblogs.com/blog/831429/202009/831429-20200910180112565-1809466339.png)
理論上來說,將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【通知觀察者】, 進入此處'
```
[歡迎造訪本人剖析vue3的github倉庫](https://github.com/chua1989/vue3-analyze)