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](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)