Vue3原始碼分析之 Ref 與 ReactiveEffect
阿新 • • 發佈:2021-12-17
Vue3中的響應式實現原理
完整 js版本簡易原始碼 在最底部
ref 與 reactive 是Vue3中的兩個定義響應式物件的API,其中reactive是通過 Proxy 來實現的,它返回物件的響應式副本,而Ref則是返回一個可變的ref物件,只有一個 .value屬性指向他內部的值,本文則重點來分析一下 Ref 的實現原理
ref:接受一個內部值並返回一個響應式且可變的 ref 物件。ref 物件僅有一個
.value
property,指向該內部值。
-
Ref依賴收集
首先我們需要了解 ReactiveEffect 類,建立這個類需要傳入一個 副作用函式 和 scheduler,在這個類中有 active、deps 兩個重要的屬性:
active:是否為啟用狀態,預設為:true
deps:所有依賴這個 effect 的響應式物件
ReactiveEffect 簡易 js 版本原始碼:
// 記錄當前活躍的物件 let activeEffect // 標記是否追蹤 let shouldTrack = false class ReactiveEffect{ active = true // 是否為啟用狀態 deps = [] // 所有依賴這個 effect 的響應式物件 onStop = null // function constructor(fn, scheduler) { this.fn = fn // 回撥函式,如: computed(/* fn */() => { return testRef.value ++ }) // function型別,不為空那麼在 TriggerRefValue 函式中會執行 effect.scheduler,否則會執行 effect.run this.scheduler = scheduler } run() { // 如果這個 effect 不需要被響應式物件收集 if(!this.active) { return this.fn() } // 原始碼這裡用了兩個工具函式:pauseTracking 和 enableTracking 來改變 shouldTrack的狀態 shouldTrack = true activeEffect = this // 在設定完 activeEffect 後執行,在方法中能夠對當前活躍的 activeEffect 進行依賴收集 const result = this.fn() shouldTrack = false // 執行完副作用函式後要清空當前活躍的 effect activeEffect = undefined return result } // 暫停追蹤 stop() { if (this.active) { // 找到所有依賴這個 effect 的響應式物件 // 從這些響應式物件裡面把 effect 給刪除掉 cleanupEffect(this) // 執行onStop回撥函式 if (this.onStop) { this.onStop(); } this.active = false; } } }
瞭解了 ReactiveEffect 類,再來分析 Ref
ref 簡易JS版本原始碼
class RefImpl{ // Set格式,儲存 effect dep // 儲存原始值 _rawValue // 標記這是一個 Ref 物件,isRef API就可以直接通過判斷這個內部屬性即可 __v_isRef = true // _shallow:這個值是為了實現 shallowRef API 而存在 // 目前這裡沒有使用到 constructor(value, _shallow) { this._value = value // 儲存原始值,用來與新值做對比 this._rawValue = _shallow ? value : toRaw(value) // _value 內部值 // toReactive => isObject(value) ? reactive(value) : value // 對 value 進行包裝 this._value = _shallow ? value : toReactive(value) } get value() { // 在獲取這個 Ref 內部值時 進行依賴收集 // trackRefValue() 依賴收集 trackRefValue(this) return this._value } }
trackRefValue方法實現
/** * activeEffect 當前活躍的 effect 物件 * shouldTrack 是否允許追蹤 * const isTracking = () => activeEffect && shouldTrack */ function trackRefValue(ref) { // 若沒有活躍的 effect 物件或 不需要進行追蹤 if(!isTracking()) { return } // 如果這個 Ref 物件還沒有對 dep 進行初始化(Ref 中 dep 屬性預設為 undefined) if(!ref.dep) { ref.dep = new Set() } trackEffects(ref.dep) } function trackEffects(dep) { // 若改 effect 物件已經收集,跳過 if(!dep.has(activeEffect)) { // 將 effect 物件新增到 Ref 物件的 dep 中 dep.add(activeEffect) // 將這個Ref的dep存放到這個 effect 的 deps 中 // 目的是為了在停止追蹤時,從 響應式物件將 effect 移除掉 activeEffect.deps.push(dep) } }
寫個 testComputed 函式來回顧 和 測試一下上面的流程
const testRef = new RefImpl(1) const testComputed = (fn) => { // 建立一個 effect 物件 const testEffect = new ReactiveEffect(fn) // 預設執行 effect.run() 函式(設定 activeEffect=this -> 執行 fn(依賴收集) -> 清空 activeEffect=undefined) return testEffect.run() // 此時,依賴情況: // testRef.dep = [ testEffect:effect ] // testEffect.deps = [ testRef.dep ] } const fn = () => { console.log('textComputed', testRef.value) } testComputed(fn)
-
Ref 觸發更新
在 RefImpl 類中增加 set value 方法
class RefImpl{ // ...... // 增加 set value () 方法 set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal) // 對比 新老值 是否相等,這裡不能簡單的使用 == 或 === // Object.is() 方法判斷兩個值是否為同一個值。 if (!Object.is(newVal, this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : toReactive(newVal) // 在內部值被改變的時候會觸發依賴更新 triggerRefValue(this) // ,newValue) 在dev環境下使用了 newValue,這裡忽略 } } // ...... }
triggerRefValue 方法實現
function triggerRefValue(ref) { triggerEffects(ref.dep); } function triggerEffects(dep) { // 執行 Ref.dep 中收集的所有 effect for (const effect of dep) { // 這裡做個判斷,執行的 effect 不是 當前活躍的 effect if(effect !== activeEffect) { if(effect.scheduler) { // effect.scheduler 文章前面有講過 /** Vue3中模板更新,就是通過建立了一個 scheduler,然後推入 微任務佇列 中去執行的 const effect = new ReactiveEffect(componentUpdateFn,() => queueJob(instance.update)) const update = (instance.update = effect.run.bind(effect) as SchedulerJob) */ effect.scheduler() } else { effect.run() } } } }
修改 testComputed 函式進行測試
const testRef = new RefImpl(1) const testComputedWatch = (fn) => { // 建立一個 effect 物件 const testEffect = new ReactiveEffect(fn) // 預設執行 effect.run() 函式(設定 activeEffect=this -> 執行 fn(依賴收集) -> 清空 activeEffect=undefined) return testEffect.run() // 此時,依賴情況: // testRef.dep = [ testEffect:effect ] // testEffect.deps = [ testRef.dep ] } const fn = () => { console.log('textComputed', testRef.value) } testComputed(fn) testRef.value ++ // -> 'textComputed', 2 testRef.value ++ // -> 'textComputed', 3
原始碼:
effect.js
// 記錄當前活躍的物件 let activeEffect // 標記是否追蹤 let shouldTrack = false // 儲存已經收集的依賴 // const targetMap = new WeakMap() const isTracking = () => activeEffect && shouldTrack function trackEffects(dep) { if(!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } } function triggerEffects(dep) { for (const effect of dep) { // 這裡做個判斷,執行的 effect 不是 當前活躍的 effect if(effect !== activeEffect) { if(effect.scheduler) { effect.scheduler() } else { effect.run() } } } } class ReactiveEffect{ active = true deps = [] onStop = null constructor(fn, scheduler) { this.fn = fn this.scheduler = scheduler } run() { if(!this.active) { return this.fn() } // 原始碼這裡用了兩個工具函式:pauseTracking 和 enableTracking 來改變 shouldTrack的狀態 shouldTrack = true activeEffect = this // 在設定完 activeEffect 後執行,在方法中能夠對當前活躍的 activeEffect 進行依賴收集 const result = this.fn() shouldTrack = false // 執行完副作用函式後要清空當前活躍的 effect activeEffect = undefined return result } // 暫停追蹤 stop() { if (this.active) { // 找到所有依賴這個 effect 的響應式物件 // 從這些響應式物件裡面把 effect 給刪除掉 cleanupEffect(this) // 執行onStop回撥函式 if (this.onStop) { this.onStop(); } this.active = false; } } } function cleanupEffect(effect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } } module.exports = { ReactiveEffect, isTracking, trackEffects, triggerEffects }
ref.js
// const { toReactive, toRaw } = require('./reactive.js') // start reactive.js 模組 const isObject = (val) => val !== null && typeof val === 'object' const toReactive = val => isObject(val) ? reactive(val) : val function toRaw(observed) { // __v_raw 是一個標誌位,表示這個是一個 reactive const raw = observed && observed['__v_raw'] return raw ? toRaw(raw) : observed } // end reactive.js const { ReactiveEffect, isTracking, trackEffects, triggerEffects } = require('./effect.js') function trackRefValue(ref) { if(!isTracking()) { return } if(!ref.dep) { ref.dep = new Set() } trackEffects(ref.dep) } function triggerRefValue(ref) { triggerEffects(ref.dep); } class RefImpl{ dep _rawValue __v_isRef = true constructor(value, _shallow) { this._value = value // 儲存原始值,用來與新值做對比 this._rawValue = _shallow ? value : toRaw(value) this._value = _shallow ? value : toReactive(value) } get value() { // 收集依賴 trackRefValue(this) return this._value } set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal) // Object.is() 方法判斷兩個值是否為同一個值。 if (!Object.is(newVal, this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : toReactive(newVal) triggerRefValue(this) // ,newValue) 在dev環境下使用了 newValue,這裡忽略 } } } function isRef(r) { return Boolean(r && r.__v_isRef === true) } function createRef(rawValue, _shallow) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, _shallow) } function ref(val) { return createRef(val, false) } module.exports = { isRef, ref }
測試程式碼
/** ** 測試程式碼 ***/ const testRef = ref(1) const textComputed = () => { const testEffect = new ReactiveEffect(() => { console.log('textComputed', testRef.value) }) testEffect.run() console.log('testEffect.deps', testEffect.deps) } const textComputed1 = () => { const fn = () => { console.log('textComputed', testRef.value) } const testEffect = new ReactiveEffect(fn) testEffect.run() } textComputed1() textComputed() console.log('testRef', testRef) // testRef.value ++