1. 程式人生 > 實用技巧 >vue3剖析:響應式原理——effect

vue3剖析:響應式原理——effect

響應式原理

原始碼目錄: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【通知觀察者】, 進入此處'

歡迎造訪本人剖析vue3的github倉庫