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