注: 爲了直觀的看到 Vue3 的實現邏輯, 本文移除了邊緣狀況處理、兼容處理、DEV環境的特殊邏輯等, 只保留了核心邏輯javascript
vue-next/reactivity 實現了 Vue3 的響應性, reactivity 提供瞭如下接口:html
export { ref, // 代理基本類型 shallowRef, // ref 的淺代理模式 isRef, // 判斷一個值是不是 ref toRef, // 把響應式對象的某個 key 轉爲 ref toRefs, // 把響應式對象的全部 key 轉爲 ref unref, // 返回 ref.value 屬性 proxyRefs, customRef, // 自行實現 ref triggerRef, // 觸發 customRef Ref, // 類型聲明 ToRefs, // 類型聲明 UnwrapRef, // 類型聲明 ShallowUnwrapRef, // 類型聲明 RefUnwrapBailTypes // 類型聲明 } from './ref' export { reactive, // 生成響應式對象 readonly, // 生成只讀對象 isReactive, // 判斷值是不是響應式對象 isReadonly, // 判斷值是不是隻讀對象 isProxy, // 判斷值是不是 proxy shallowReactive, // 生成淺響應式對象 shallowReadonly, // 生成淺只讀對象 markRaw, // 讓數據不可被代理 toRaw, // 獲取代理對象的原始對象 ReactiveFlags, // 類型聲明 DeepReadonly // 類型聲明 } from './reactive' export { computed, // 計算屬性 ComputedRef, // 類型聲明 WritableComputedRef, // 類型聲明 WritableComputedOptions, // 類型聲明 ComputedGetter, // 類型聲明 ComputedSetter // 類型聲明 } from './computed' export { effect, // 定義反作用函數, 返回 effect 自己, 稱爲 runner stop, // 中止 runner track, // 收集 effect 到 Vue3 內部的 targetMap 變量 trigger, // 執行 targetMap 變量存儲的 effects enableTracking, // 開始依賴收集 pauseTracking, // 中止依賴收集 resetTracking, // 重置依賴收集狀態 ITERATE_KEY, // 固定參數 ReactiveEffect, // 類型聲明 ReactiveEffectOptions, // 類型聲明 DebuggerEvent // 類型聲明 } from './effect' export { TrackOpTypes, // track 方法的 type 參數的枚舉值 TriggerOpTypes // trigger 方法的 type 參數的枚舉值 } from './operations'
target: 普通的 JS 對象vue
reactive: @vue/reactivity
提供的函數, 接收一個對象, 並返回一個 代理對象, 即響應式對象java
shallowReactive: @vue/reactivity
提供的函數, 用來定義淺響應對象python
readonly:@vue/reactivity
提供的函數, 用來定義只讀對象react
shallowReadonly: @vue/reactivity
提供的函數, 用來定義淺只讀對象git
handlers: Proxy 對象暴露的鉤子函數, 有 get()
、set()
、deleteProperty()
、ownKeys()
等, 能夠參考MDNgithub
targetMap: @vue/reactivity
內部變量, 存儲了全部依賴數組
effect: @vue/reactivit
提供的函數, 用於定義反作用, effect(fn, options)
的參數就是反作用函數緩存
watchEffect: @vue/runtime-core
提供的函數, 基於 effect 實現
track: @vue/reactivity
內部函數, 用於收集依賴
trigger: @vue/reactivity
內部函數, 用於消費依賴
scheduler: effect 的調度器, 容許用戶自行實現
先看下邊的流程簡圖, 圖中 Vue 代碼的功能是: 每隔一秒在 id
爲 Box
的 div
中輸出當前時間
在開始梳理 Vue3 實現響應式的步驟以前, 要先簡單理解 effect
, effect
是響應式系統的核心, 而響應式系統又是 Vue3 的核心
上圖中從 track
到 targetMap
的黃色箭頭, 和從 targetMap
到 trigger
的白色箭頭, 就是 effect
函數要處理的環節
effect
函數的語法爲:
effect(fn, options)
effect
接收兩個參數, 第一個必填參數 fn
是反作用函數
第二個選填 options
的參數定義以下:
export interface ReactiveEffectOptions { lazy?: boolean // 是否延遲觸發 effect scheduler?: (job: ReactiveEffect) => void // 調度函數 onTrack?: (event: DebuggerEvent) => void // 追蹤時觸發 onTrigger?: (event: DebuggerEvent) => void // 觸發回調時觸發 onStop?: () => void // 中止監聽時觸發 allowRecurse?: boolean // 是否容許遞歸 }
下邊從流程圖中左上角的 Vue 代碼開始
經過 reactive
方法將 target
對象轉爲響應式對象, reactive
方法的實現方法以下:
import { mutableHandlers } from './baseHandlers' import { mutableCollectionHandlers } from './collectionHandlers' const reactiveMap = new WeakMap<Target, any>() const readonlyMap = new WeakMap<Target, any>() export function reactive(target: object) { return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers ) } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { const proxyMap = isReadonly ? readonlyMap : reactiveMap const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } const targetType = getTargetType(target) // 先忽略, 上邊例子中, targetType 的值爲: 1 const proxy = new Proxy( target, targetType === 2 ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy }
reactive
方法攜帶 target
對象和 mutableHandlers
、mutableCollectionHandlers
調用 createReactiveObject
方法, 這兩個 handers 先忽略
createReactiveObject
方法經過 reactiveMap
變量緩存了一份響應式對象, reactiveMap
和 readonlyMap
變量是文件內部的變量, 至關於文件級別的閉包變量
其中 targetType 有三種枚舉值: 0 表明不合法, 1 表明普通對象, 2 表明集合, 圖中例子中, targetType
的值爲 1, 對於 { text: '' }
這個普通對象傳進 reactive()
方法時, 使用 baseHandlers
提供的 mutableHandlers
最後調用 Proxy 方法將 target 轉爲響應式對象, 其中 "響應" 體如今 handers 裏, 能夠這樣理解: reactive = Proxy (target, handlers)
mutableHandlers
負責掛載 get
、set
、deleteProperty
、has
、ownKeys
這五個方法到響應式對象上
其中 get
、has
、ownKeys
負責收集依賴, set
和 deleteProperty
負責消費依賴
響應式對象的 get
、has
和 ownKeys
方法被觸發時, 會調用 createGetter
方法, createGetter
的實現以下:
function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { const res = Reflect.get(target, key, receiver) if (!isReadonly) { track(target, TrackOpTypes.GET, key) } if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) } return res } }
當 { text: '' }
這個普通JS對象傳到 createGetter
時, key 的值爲: text
, res 的值爲: String
類型, 若是 res 的值爲 Object
類型則會遞歸調用, 將 res 轉爲響應式對象
createGetter
方法的目的是觸發 track
方法, 對應本文的第 3 步
響應式對象的 set
和 deleteProperty
方法被觸發時, 會調用 createSetter
方法, createSetter
的實現以下:
function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] const result = Reflect.set(target, key, value, receiver) trigger(target, TriggerOpTypes.SET, key, value, oldValue) return result } }
createSetter
方法的目的是觸發 trigger
方法, 對應本文的第 4 步
這一步是整個響應式系統最關鍵的一步, 即咱們常說的依賴收集, 依賴收集的概念很簡單, 就是把 響應式數據 和 反作用函數 創建聯繫
文章一開始流程圖的例子中, 就是把 target
對象和 document.getElementById("Box").innerText = date.text;
這個反作用函數創建關聯, 這個 "關聯" 指的就是上邊提到的 targetMap
變量, 後邊會詳細描述一下 targetMap
對象的結構
第 2 步介紹了 createGetter
方法的核心是調用 track
方法, track
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 track
的實現:
const targetMap = new WeakMap<any, KeyToDepMap>() // target: { text: '' } // type: get // key: text export function track(target: object, type: TrackOpTypes, key: unknown) { let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = new Set())) } if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } }
從 track
方法咱們能看到 targetMap
這個閉包變量上儲存了全部的 effect
, 換句話說是把能影響到 target
的反作用函數收集到 targetMap
變量中
targetMap 是個 WeakMap, WeakMap 和 Map 的區別在於 WeakMap 的鍵只能是對象, 用 WeakMap 而不用 Map 是由於 Proxy 對象不能代理普通數據類型
targetMap 的結構:
const targetMap = { [target]: { [key1]: [effect1, effect2, effect3, ...], [key2]: [effect1, effect2, effect3, ...] } }
{ text: '' }
這個target 傳進來時, targetMap 的結構是:
// 上邊例子中用來在 id 爲 Box 的 div 中輸出當前時間的反作用函數 const effect = () => { document.getElementById("Box").innerText = date.text; }; const target = { "{ text: '' }": { "text": [effect] } }
舉三個例子, 來分析一下 targetMap 的結構, 第一個例子是多個 target 狀況:
<script> import { effect, reactive } from "@vue/reactivity"; const target1 = { language: "JavaScript"}; const target2 = { language: "Go"}; const target3 = { language: "Python"}; const r1 = reactive(target1); const r2 = reactive(target2); const r3 = reactive(target3); // effect1 effect(() => { console.log(r1.language); }); // effect2 effect(() => { console.log(r2.language); }); // effect3 effect(() => { console.log(r3.language); }); // effect4 effect(() => { console.log(r1.language); console.log(r2.language); console.log(r3.language); }); </script>
這種狀況下 targetMap 的構成是:
const effect1 = () => { console.log(r1.language); }; const effect2 = () => { console.log(r2.language); }; const effect3 = () => { console.log(r3.language); }; const effect4 = () => { console.log(r1.language); console.log(r2.language); console.log(r3.language); }; const targetMap = { '{"language":"JavaScript"}': { "language": [effect1, effect4] }, '{"language":"Go"}': { "language": [effect2, effect4] }, '{"language":"Python"}': { "language": [effect3, effect4] } }
第二個例子是單個 target 多個屬性時:
import { effect, reactive } from "@vue/reactivity"; const target = { name: "rmlzy", age: "27", email: "rmlzy@outlook.com"}; const user = reactive(target); effect(() => { console.log(user.name); console.log(user.age); console.log(user.email); });
這種狀況下 targetMap 的構成是:
const effect = () => { console.log(user.name); console.log(user.age); console.log(user.email); }; const targetMap = { '{"name":"rmlzy","age":"27","email":"rmlzy@outlook.com"}': { "name": [effect], "age": [effect], "email": [effect] } }
第三個例子是多維對象時:
import { effect, reactive } from "@vue/reactivity"; const target = { name: "rmlzy", skills: { frontend: ["JS", "TS"], backend: ["Node", "Python", "Go"] } }; const user = reactive(target); // effect1 effect(() => { console.log(user.name); }); // effect2 effect(() => { console.log(user.skills); }); // effect3 effect(() => { console.log(user.skills.frontend); }); // effect4 effect(() => { console.log(user.skills.frontend[0]); });
這種狀況下 targetMap 的構成是:
const effect1 = () => { console.log(user.name); }; const effect2 = () => { console.log(user.skills); }; const effect3 = () => { console.log(user.skills.frontend); }; const effect4 = () => { console.log(user.skills.frontend[0]); }; const targetMap = { '{"name":"rmlzy","skills":{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}}': { "name": [effect1], "skills": [effect2, effect3, effect4] }, '{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}': { "frontend": [effect3, effect4] } }
第 3 步的目的是收集依賴, 這一步的目的是消費依賴
這裏要注意, 只有當 target 代理對象的 get
方法被觸發時, 纔會真正執行 track
, 換句話說, 沒有地方須要 get
target 對象時, target 沒有依賴, 也就沒有收集依賴一說
下邊的例子中只是把 target 轉換爲了響應式對象, 並無觸發依賴收集, targetMap 是空的
const target = {"text": ""}; const date = reactive(target); effect(() => { date.text = new Date().toString(); });
第 2 步介紹了 createSetter
方法的核心是調用 trigger
方法, trigger
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 trigger
的實現:
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { const depsMap = targetMap.get(target) if (!depsMap) { // never been tracked return } const effects = new Set<ReactiveEffect>() if (isMap(target)) { effects.add(depsMap.get(ITERATE_KEY)) } const run = (effect: ReactiveEffect) => { if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } effects.forEach(run) }
trigger 的實現很簡單, 先把 target 相關的 effect 彙總到 effects 數組中, 而後調用 effects.forEach(run)
執行全部的反作用函數
再回顧一下 effect 方法的定義: effect(fn, options)
, 其中 options 有個可選屬性叫 scheduler
, 從上邊 run
函數也能夠看到 scheduler
的做用是讓用戶自定義如何執行反作用函數
又回到了本文最開始講的 effect, effect 函數的實現以下:
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }
effect 的核心是調用 createReactiveEffect
方法
能夠看到 options.lazy
默認爲 false
會直接執行 effect, 當設置爲 true
時, 會返回 effect 由用戶手動觸發
createReactiveEffect
函數的實現以下:
const effectStack: ReactiveEffect[] = [] let activeEffect: ReactiveEffect | undefined function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (!effect.active) { return options.scheduler ? undefined : fn() } if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !!options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect }
首先定義了 effect 是個普通的 function
, 先看後邊 effect 函數掛載的屬性:
effect.id = uid++ // 自增ID, 每一個 effect 惟一的ID effect.allowRecurse = !!options.allowRecurse // 是否容許遞歸 effect._isEffect = true // 特殊標記 effect.active = true // 激活狀態 effect.deps = [] // 依賴數組 effect.raw = fn // 緩存一份用戶傳入的反作用函數 effect.options = options // 緩存一份用戶傳入的配置
isEffect
函數用來判斷值是不是 effect, 就是根據上邊 _isEffect
變量判斷的, isEffect
函數實現以下:
function isEffect(fn) { return fn && fn._isEffect === true; }
再來看 effect 的核心邏輯:
cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] }
effectStack
用數組實現棧,activeEffect
是當前生效的 effect
先執行 cleanup(effect)
:
function cleanup(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } }
cleanup
的目的是清空 effect.deps
, deps
是持有該 effect 的依賴數組, deps
的結構以下
清除完依賴後, 開始從新收集依賴, 把當前 effect 追加到 effectStack, 將 activeEffect 設置爲當前的 effect, 而後調用 fn 而且返回 fn() 的結果
第 4 步提過到: "只有當 target 代理對象的 get
方法被觸發時, 纔會真正執行 track
", 至此纔是真正的觸發了 target
代理對象的 get
方法, 執行了track
方法而後收集到了依賴
等到 fn
執行結束, finally 階段, 把當前的 effect 彈出, 恢復 effectStack 和 activeEffect, Vue3 整個響應式的流程到此結束
個人理解是爲了暴露給 onTrack
方法, 來總體看一下 activeEffect 出現的地方:
let activeEffect; function effect(fn, options = EMPTY_OBJ) { const effect = createReactiveEffect(fn, options); return effect; } function createReactiveEffect(fn, options) { const effect = function reactiveEffect() { // 省略部分代碼 ... try { activeEffect = effect; return fn(); } finally { activeEffect = effectStack[effectStack.length - 1]; } }; // 省略部分代碼 ... return effect; } function track(target, type, key) { if (activeEffect === undefined) { return; } let dep = targetMap.get(target).get(key); // dep 是存儲 effect 的 Set 數組 if (!dep.has(activeEffect)) { dep.add(activeEffect); activeEffect.deps.push(dep); if (activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }); } } }
在 fn
執行前, activeEffect
被賦值爲當前 effect
在 fn
執行時的依賴收集階段, 獲取 targetMap 中的 dep (存儲 effect 的 Set 數組), 並暴露給 options.onTrack
接口
@vue/reactivity
提供了 stop 函數, effect
能夠被 stop 函數終止
const obj = reactive({ foo: 0 }); const runner = effect(() => { console.log(obj.foo); }); // effect 被執行一次, 輸出 0 // obj.foo 被賦值一次, effect 被執行一次, 輸出 1 obj.foo ++; // 中止 effect stop(runner); // effect 不會被觸發, 無輸出 obj.foo ++;
watchEffect
來自 @vue/runtime-core
, effect
來自 @vue/reactivity
watchEffect
基於 effect
實現watchEffect
會維護與組件實例的關係, 若是組件被卸載, watchEffect
會被 stop
, 而 effect
不會被 stop
watchEffect
接收的反作用函數, 會攜帶一個 onInvalidate
的回調函數做爲參數, 這個回調函數會在反作用無效時執行
watchEffect(async (onInvalidate) => { let valid = true; onInvalidate(() => { valid = false; }); const data = await fetch(obj.foo); if (valid) { // 獲取到 data } else { // 丟棄 } });
JS數據類型:
由於 Proxy 只能代理對象, reactive
函數的核心又是 Proxy, 因此 reactive 不能代理基本類型
對於基本類型須要用 ref 函數將基本類型轉爲對象:
class RefImpl<T> { private _value: T public readonly __v_isRef = true constructor(private _rawValue: T, public readonly _shallow = false) { this._value = _shallow ? _rawValue : convert(_rawValue) } get value() { track(toRaw(this), TrackOpTypes.GET, 'value') return this._value } set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : convert(newVal) trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal) } } }
其中 __v_isRef
參數用來標誌當前值是 ref 類型, isRef
的實現以下:
export function isRef(r: any): r is Ref { return Boolean(r && r.__v_isRef === true) }
這樣作有個缺點, 須要多取一層 .value
:
const myRef = ref(0); effect(() => { console.log(myRef.value); }); myRef.value = 1;
這也是 Vue ref 語法糖提案的緣由, 能夠參考 如何評價 Vue 的 ref 語法糖提案?
shallowReactive
用來定義淺響應數據, 深層次的對象值是非響應式的:
const target = { foo: { bar: 1 } }; const obj = shallowReactive(target); effect(() => { console.log(obj.foo.bar); }); obj.foo.bar = 2; // 無效, reactive 則有效 obj.foo = { bar: 2 }; // 有效
相似 shallowReactive
, 深層次的對象值是能夠被修改的
markRaw 的做用是讓數據不可被代理, 全部攜帶 __v_skip
屬性, 而且值爲 true
的數據都會被跳過:
export function markRaw<T extends object>(value: T): T { def(value, ReactiveFlags.SKIP, true) return value }
toRaw 的做用是獲取代理對象的原始對象:
const obj = {}; const reactiveProxy = reactive(obj); console.log(toRaw(reactiveProxy) === obj); // true
const myRef = ref(0); const myRefComputed = computed(() => { return myRef.value * 2; }); effect(() => { console.log(myRef.value * 2); });
當 myRef
值變化時, computed 會執行一次, effect 會執行一次
當 myRef
值未變化時, computed 不會執行, effect 依舊會執行
若是你有問題歡迎留言和我交流, 閱讀原文