Vue3 Reactivity 數據響應式原理解析
Vue3 如火如荼,與其乾等,不如花一個下午茶的時間來看下最新的響應式數據是如何實現的吧。在本文中,會寫到 vue3 的依賴收集和 proxy 數據代理,以及副作用 (effect) 是如何進行工作的。
基本差不多了,圖有點小丑,也可以看比人比較全的圖。QAQ
前言
好久沒有接觸Vue
了,在前幾天觀看尤大的直播時談論對於看源碼
的一些看法,是爲了更好的上手vue
? 還是想要學習內部的框架思想?
國內前端:面試,面試會問。
在大環境下似乎已經卷到了只要你是開發者,那麼必然需要去學習源碼,不論你是實習生,還是應屆生,或者是多年經驗的老前端。
如果你停滯下來,不跟着卷,那麼忽然之間帶來的壓力就會將你沖垮,以至於你可能很難在內卷的環境下生存下去,哪怕你是對的。
有興趣的話可以閱讀一下 @掘金泥石流大佬的寫的程序員焦慮程度自測表。
似乎講了太多的題外話,與其發牢騷不如靜下心來,一起學習一下Reactivity
的一些基本原理吧,相信閱讀完文章的你會對vue 3
數據響應式有更加深刻的理解。
而之所以選擇
Reactivity
模塊來說,是因爲其耦合度較低,且是vue3.0
核心模塊之一,性價比成本非常高。
基礎篇
在開始之前,如果不瞭解ES6
出現的一些高階api
,如,Proxy
, Reflect
, WeakMap
, WeakSet
,Map
, Set
等等可以自行翻閱到資源章節,先了解前置知識點在重新觀看爲最佳。
Proxy
在@vue/reactivity
中,Proxy
是整個調度的基石。
通過Proxy
代理對象,才能夠在get
, set
方法中完成後續的事情,比如依賴收集
,effect
,track
, trigger
等等操作,在這裏就不詳細展開,後續會詳細展開敘述。
如果有同學迫不及待,加上天資聰慧,
ES6
有一定基礎,可以直接跳轉到原理篇進行觀看和思考。
先來手寫一個簡單的Proxy
。在其中handleCallback
中寫了了set
, get
兩個方法,又來攔截當前屬性值變化的數據監聽。先上代碼:
const user = {
name: 'wangly19',
age: 22,
description: '一名掉頭髮微乎其微的前端小哥。'
}
const userProxy = new Proxy(user, {
get(target, key) {
console.log(`userProxy: 當前獲取key爲${key}`)
if (target.hasOwnProperty(key)) return target[key]
return {
}
},
set(target, key, value) {
console.log(`userProxy: 當前設置值key爲${key}, value爲${value}`)
let isWriteSuccess = false
if (target.hasOwnProperty(key)) {
target[key] = value
isWriteSuccess = true
}
return isWriteSuccess
}
})
console.log('myNaame', userProxy.name)
userProxy.age = 23
複製代碼
當我們在對值去進行賦值修改和打印的時候,分別觸發了當前的set
和get
方法。
這一點非常重要,對於其他的一些屬性和使用方法在這裏就不過多的贅述,
Reflect
Reflect
並不是一個類,是一個內置的對象。這一點呢大家要知悉,不要直接實例化(new)
使用,它的功能比較和Proxy
的handles
有點類似,在這一點基礎上又添加了很多Object
的方法。
在這裏我們不去深究
Reflect
, 如果想要深入瞭解功能的同學,可以在後續資源中找到對應地址進行學習。在本章主要介紹了通過Reflect
安全的操作對象。
以下是對user
對象的一些修改
操作的實例,可以參考一下,在後續可能會用到。
const user = {
name: 'wangly19',
age: 22,
description: '一名掉頭髮微乎其微的前端小哥。'
}
console.log('change age before' , Reflect.get(user, 'age'))
const hasChange = Reflect.set(user, 'age', 23)
console.log('set user age is done? ', hasChange ? 'yes' : 'no')
console.log('change age after' , Reflect.get(user, 'age'))
const hasDelete = Reflect.deleteProperty(user, 'age')
console.log('delete user age is done?', hasDelete ? 'yes' : 'none')
console.log('delete age after' , Reflect.get(user, 'age'))
複製代碼
原理篇
當了解了前置的一些知識後,就要開始@vue/reactivity
的源碼解析篇章了。下面開始會以簡單的思路來實現一個基礎的reactivity
,當你瞭解其本質原理後,你會對@vue/reactivity
的依賴收集(track)
和觸發更新(trigger)
,以及副作用(effect)
究竟是什麼工作。
reactive
reactive
是vue3
中用於生成引用類型
的api
。
const user = reactive({
name: 'wangly19',
age: 22,
description: '一名掉頭髮微乎其微的前端小哥。'
})
複製代碼
那麼往函數內部看看,reactive
方法究竟做了什麼?
在內部,對傳入的對象進行了一個target
的只讀判斷,如果你傳入的target
是一個只讀代理的話,會直接返回掉。對於正常進行reactive
的話則是返回了createReactiveObject
方法的值。
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
複製代碼
createReactiveObject
在createReactiveObject
中,做的事情就是爲target
添加一個proxy
代理。這是其核心,reactive
最終拿到的是一個proxy
代理,參考Proxy
章節的簡單事例就可以知道reactive
是如何進行工作的了,那麼在來看下createReactiveObject
做了一些什麼事情。
首先先判斷當前target
的類型,如果不符合要求,直接拋出警告並且返回原來的值。
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
複製代碼
其次判斷當前對象是否已經被代理且並不是只讀的,那麼本身就是一個代理對象,那麼就沒有必要再去進行代理了,直接將其當作返回值返回,避免重複代理。
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
複製代碼
對於這些判斷代碼來說,閱讀起來並不是很困難,注意if ()
中判斷的條件,看看它做了一些什麼動作即可。而createReactiveObject
做的最重要的事情就是創建target
的proxy
, 並將其放到Map
中記錄。
而比較有意思的是其中對傳入的target
調用了不同的proxy handle
。那麼就一起來看看handles
中究竟幹了一些什麼吧。
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
複製代碼
handles 的類型
在對象類型中,將Object
和Array
與Map
,Set
, WeakMap
,WeakSet
區分開來了。它們調用的是不同的Proxy Handle
。
-
baseHandlers.ts
:Object
&Array
會調用此文件下的mutableHandlers
對象作爲Proxy Handle
。 -
collectionHandlers.ts
:Map
,Set
,WeakMap
,WeakSet
會調用此文件下的mutableCollectionHandlers
對象作爲Proxy Handle
。
/**
* 對象類型判斷
* @lineNumber 41
*/
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
複製代碼
會在new Proxy
的根據返回的targetType
判斷。
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
複製代碼
由於篇幅有限,下文中只舉例
mutableHandlers
當作分析的參考。當理解mutableHandlers
後對於collectionHandlers
只是時間的問題。
Proxy Handle
在上面說到了根據不同的Type
調用不同的handle
,那麼一起來看看mutableHandlers
究竟做了什麼吧。
在基礎篇中,都知道Proxy
可以接收一個配置對象,其中我們演示了get
和set
的屬性方法。而mutableHandlers
就是何其相同意義的事情,在內部分別定義get
, set
, deleteProperty
, has
, oneKeys
等多個屬性參數,如果不知道什麼含義的話,可以看下Proxy Mdn
。在這裏你需要理解被監聽的數據
只要發生增查刪改
後,絕大多數都會進入到對應的回執通道里面。
在這裏,我們用簡單的get
, set
來進行簡單的模擬實例。
function createGetter () {
return (target, key, receiver) => {
const result = Reflect.get(target, key, receiver)
track(target, key)
return result
}
}
const get = /*#__PURE__*/ createGetter()
function createSetter () {
return (target, key, value, receiver) => {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (result && oldValue != value) {
trigger(target, key)
}
return result
}
}
複製代碼
在get
的時候會進行一個track
的依賴收集,而set
的時候則是觸發trigger
的觸發機制。在vue3
,而trigger
和track
的話都是在我們effect.ts
當中聲明的,那麼接下來就來看看依賴收集
和響應觸發
究竟做了一些什麼吧。
Effect
對於整個 effect 模塊,將其分爲三個部分來去閱讀:
-
effect
:副作用函數 -
teack
: 依賴收集,在proxy
代理數據get
時調用 -
trigger
: 觸發響應,在proxy
代理數據發生變化的時候調用。
effect
通過一段實例來看下effect
的使用,並且瞭解它主要參數是一個函數。在函數內部會幫你執行一些副作用記錄和特性判斷。
effect(() => {
proxy.user = 1
})
複製代碼
來看看vue
的effect
幹了什麼?
在這裏,首先判斷當前參數fn
是否是一個effect
,如果是的話就將raw
中存放的fn
進行替換。然後重新進行createReactiveEffect
生成。
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
}
複製代碼
在createReactiveEffect
會將我們effect
推入到effectStack
中進行入棧操作,然後用activeEffect
進行存取當前執行的effect
,在執行完後會將其進行出棧
。同時替換activeEffect
爲新的棧頂。
而在effect
執行的過程中就會觸發proxy handle
然後track
和trigger
兩個關鍵的函數。
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(eff) {
try {
effectStack.push(eff)
activeEffect = eff
return eff(...argsument)
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
複製代碼
track(依賴收集)
在track
的時候,會進行我們所熟知的依賴收集,會將當前activeEffect
添加到dep
裏面,而說起這一類的關係。它會有一個一對多對多的關係。
從代碼看也非常的清晰,首先我們會有一個一個總的targetMap
它是一個WeakMap
,key
是target(代理的對象)
, value
是一個Map
,稱之爲depsMap
,它是用於管理當前target
中每個key
的deps
也就是副作用依賴,也就是以前熟知的depend
。在vue3
中是通過Set
來去實現的。
第一步先憑藉當前target
獲取targetMap
中的depsMap
,如果不存在就進行targetMap.set(target, (depsMap = new Map()))
初始化聲明,其次就是從depsMap
中拿當前key
的deps
, 如果沒有找到的話,同樣是使用depsMap.set(key, (dep = new Set()))
進行初始化聲明,最後將當前activeEffect
推入到deps
, 進行依賴收集。
- 在
targetMap
中找target
- 在
depsMap
中找key
- 將
activeEffect
保存到dep
裏面。
這樣的話就會形成一個一對多對多的結構模式,裏面存放的是所有被proxy
劫持的依賴。
function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
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)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
複製代碼
trigger(響應觸發)
在trigger
的時候,做的事情其實就是觸發當前響應依賴的執行。
首先,需要獲取當前key
下所有渠道的deps
,所以會看到有一個effects
和add
函數, 做的事情非常的簡單,就是來判斷當前傳入的depsMap
的屬性是否需要添加到effects
裏面,在這裏的條件就是effect
不能是當前的activeEffect
和effect.allowRecurse
,來確保當前set key
的依賴都進行執行。
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
複製代碼
下面下面熟知的場景就是判斷當前傳入的一些變化行爲,最常見的就是在trigger
中會傳遞的TriggerOpTypes
行爲,然後執行add
方法將其將符合條件的effect
添加到effects
當中去,在這裏@vue/reactivity
做了很多數據就變異上的行爲,如length
變化。
然後根據不同的TriggerOpTypes
進行depsMap
的數據取出,最後放入effects
。隨後通過run
方法將當前的effect
執行,通過effects.forEach(run)
進行執行。
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
複製代碼
而run
又做了什麼呢?
首先就是判斷當前effect
中options
下有沒有scheduler
,如果有的話就使用schedule
來處理執行,反之直接直接執行effect()
。
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
複製代碼
將其縮短一點看處理邏輯,其實就是從targetMap
中拿對應key
的依賴。
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const dep = depsMap.get(key)
if (dep) {
dep.forEach((effect) => {
effect()
})
}
複製代碼
Ref
衆所周知,ref
是vue3
對普通類型的一個響應式數據聲明。而獲取ref
的值需要通過ref.value
的方式進行獲取,很多人以爲ref
就是一個簡單的reactive
但其實不是。
在源碼中,ref
最終是調用一個createRef
的方法,在其內部返回了RefImpl
的實例。它與Proxy
不同的是,ref
的依賴收集和響應觸發是在getter/setter
當中,這一點可以參考圖中demo
形式,鏈接地址 gettter/setter。
export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
複製代碼
如圖所示,vue
在getter
中與proxy中的get
一樣都調用了track
收集依賴,在setter
中進行_value
值更改後調用trigger
觸發器。
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)
}
}
}
複製代碼
那麼你現在應該知道:
-
proxy handle
是reactive
的原理,而ref
的原理是getter/setter
。 -
在
get
的時候都調用了track
,set
的時候都調用了trigger
-
effect
是數據響應的核心。
Computed
computed
一般有兩種常見的用法, 一種是通過傳入一個對象,內部有set
和get
方法,這種屬於ComputedOptions
的形式。
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
)
複製代碼
而在內部會有getter / setter
兩個變量來進行保存。
當getterOrOptions
爲函數的時候,會將其賦值給與getter
。
當getterOrOptions
爲對象的時候,會將set
和get
分別賦值給setter
,getter
。
隨後將其作爲參數進行實例化ComputedRefImpl
類,並將其當作返回值返回出去。
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
複製代碼
那麼ComputedRefImpl
幹了一些什麼?
計算屬性的源碼,其實絕大多數是依賴前面對effect
的一些理解。
首先,我們都知道,effect
可以傳遞一個函數
和一個對象options
。
在這裏將getter
當作函數參數傳遞,也就是副作用
,而在options
當中配置了lazy
和scheduler
。
lazy
表示effect
並不會立即被執行,而scheduler
是在trigger
中會判斷你是否傳入了scheduler
,傳入後就執行scheduler
方法。
而在computed scheduler
當中,會判斷當前的_dirty
是否爲false
,如果是的話會把_dirty
設置爲true
,且執行trigger
觸發響應。
class ComputedRefImpl<T> {
private _value!: T
private _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
this[ReactiveFlags.IS_READONLY] = isReadonly
}
複製代碼
而在getter/setter
中會對_value
進行不同操作。
首先,在get value
中,判斷當前._dirty
是否爲true
,如果是的話執行緩存的effect
並將其返回結果存放到_value
,並執行track
進行依賴收集。
其次,在set value
中,則是調用_setter
方法重新新值。
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
if (self._dirty) {
self._value = this.effect()
self._dirty = false
}
track(self, TrackOpTypes.GET, 'value')
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
複製代碼
資源引用
下面是一些參考資源,有興趣的小夥伴可以看下
-
ES6 系列之 WeakMap
-
Proxy 和 Reflect
-
Vue Mastery
-
Vue Docs
-
React 中引入 Vue3 的 @vue/reactivity 實現響應式狀態管理
總結
如果你使用vue
的話強烈建議自己debug
將這一塊看完,絕對會對你寫代碼有很大的幫助。vue3
如火如荼,目前已經有團隊作用於生產環境進行項目開發,社區的生態也慢慢的發展起來。
@vue/reactivity
的閱讀難度並不高,也有很多優質的教程,有一定的工作基礎和代碼知識都能循序漸進的理解下來。我個人其實並不需要將其理解的滾瓜爛熟,理解每一行代碼的意思什麼的,而是瞭解其核心思想,學習框架理念以及一些框架開發者代碼寫法的思路。這都是能夠借鑑並將其吸收成爲自己的知識。
對於一個已經轉到
React
生態體系下的前端來說,讀Vue
的源碼其實更多的是豐富自己在思維上的知識,而不是爲了面試而去讀的。正如同你背書不是爲了考試,而是學習知識。在現在的環境下,很難做到這些事情,靜下心來專心理解一件知識不如背幾篇面經。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/V_959uAoCQ9UTE9NB4tIpg