Vue3最啰嗦的Reactivity數(shù)據(jù)響應式原理解析
Vue3 如火如荼,與其干等,不如花一個下午茶的時間來看下最新的響應式數(shù)據(jù)是如何實現(xiàn)的吧。在本文中,會寫到 vue3 的依賴收集和 proxy 數(shù)據(jù)代理,以及副作用 (effect) 是如何進行工作的。

基本差不多了,圖有點小丑,也可以看比人比較全的圖。QAQ

前言
好久沒有接觸Vue了,在前幾天觀看尤大的直播時談論對于看源碼的一些看法,是為了更好的上手vue? 還是想要學習內部的框架思想?

國內前端:面試,面試會問。
在大環(huán)境下似乎已經(jīng)卷到了只要你是開發(fā)者,那么必然需要去學習源碼,不論你是實習生,還是應屆生,或者是多年經(jīng)驗的老前端。
如果你停滯下來,不跟著卷,那么忽然之間帶來的壓力就會將你沖垮,以至于你可能很難在內卷的環(huán)境下生存下去,哪怕你是對的。
有興趣的話可以閱讀一下 @掘金泥石流大佬的寫的程序員焦慮程度自測表。
似乎講了太多的題外話,與其發(fā)牢騷不如靜下心來,一起學習一下Reactivity的一些基本原理吧,相信閱讀完文章的你會對vue 3數(shù)據(jù)響應式有更加深刻的理解。
而之所以選擇
Reactivity模塊來說,是因為其耦合度較低,且是vue3.0核心模塊之一,性價比成本非常高。
基礎篇
在開始之前,如果不了解ES6出現(xiàn)的一些高階api,如,Proxy, Reflect, WeakMap, WeakSet,Map, Set等等可以自行翻閱到資源章節(jié),先了解前置知識點在重新觀看為最佳。
Proxy
在@vue/reactivity中,Proxy是整個調度的基石。
通過Proxy代理對象,才能夠在get, set方法中完成后續(xù)的事情,比如依賴收集,effect,track, trigger等等操作,在這里就不詳細展開,后續(xù)會詳細展開敘述。
如果有同學迫不及待,加上天資聰慧,
ES6有一定基礎,可以直接跳轉到原理篇進行觀看和思考。
先來手寫一個簡單的Proxy。在其中handleCallback中寫了了set, get兩個方法,又來攔截當前屬性值變化的數(shù)據(jù)監(jiān)聽。先上代碼:
const user = {
name: 'wangly19',
age: 22,
description: '一名掉頭發(fā)微乎其微的前端小哥。'
}
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
復制代碼
當我們在對值去進行賦值修改和打印的時候,分別觸發(fā)了當前的set和get方法。
這一點非常重要,對于其他的一些屬性和使用方法在這里就不過多的贅述,

Reflect
Reflect并不是一個類,是一個內置的對象。這一點呢大家要知悉,不要直接實例化(new)使用,它的功能比較和Proxy的handles有點類似,在這一點基礎上又添加了很多Object的方法。
在這里我們不去深究
Reflect, 如果想要深入了解功能的同學,可以在后續(xù)資源中找到對應地址進行學習。在本章主要介紹了通過Reflect安全的操作對象。

以下是對user對象的一些修改操作的實例,可以參考一下,在后續(xù)可能會用到。
const user = {
name: 'wangly19',
age: 22,
description: '一名掉頭發(fā)微乎其微的前端小哥。'
}
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的源碼解析篇章了。下面開始會以簡單的思路來實現(xiàn)一個基礎的reactivity,當你了解其本質原理后,你會對@vue/reactivity的依賴收集(track)和觸發(fā)更新(trigger),以及副作用(effect)究竟是什么工作。
reactive
reactive是vue3中用于生成引用類型的api。
const user = reactive({
name: 'wangly19',
age: 22,
description: '一名掉頭發(fā)微乎其微的前端小哥。'
})
復制代碼
那么往函數(shù)內部看看,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章節(jié)的簡單事例就可以知道reactive是如何進行工作的了,那么在來看下createReactiveObject做了一些什么事情。
首先先判斷當前target的類型,如果不符合要求,直接拋出警告并且返回原來的值。
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
復制代碼
其次判斷當前對象是否已經(jīng)被代理且并不是只讀的,那么本身就是一個代理對象,那么就沒有必要再去進行代理了,直接將其當作返回值返回,避免重復代理。
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
復制代碼
對于這些判斷代碼來說,閱讀起來并不是很困難,注意if ()中判斷的條件,看看它做了一些什么動作即可。而createReactiveObject做的最重要的事情就是創(chuàng)建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區(qū)分開來了。它們調用的是不同的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的根據(jù)返回的targetType判斷。
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
復制代碼
由于篇幅有限,下文中只舉例
mutableHandlers當作分析的參考。當理解mutableHandlers后對于collectionHandlers只是時間的問題。
Proxy Handle
在上面說到了根據(jù)不同的Type調用不同的handle,那么一起來看看mutableHandlers究竟做了什么吧。
在基礎篇中,都知道Proxy可以接收一個配置對象,其中我們演示了get和set的屬性方法。而mutableHandlers就是何其相同意義的事情,在內部分別定義get, set, deleteProperty, has, oneKeys等多個屬性參數(shù),如果不知道什么含義的話,可以看下Proxy Mdn。在這里你需要理解被監(jiān)聽的數(shù)據(jù) 只要發(fā)生增查刪改后,絕大多數(shù)都會進入到對應的回執(zhí)通道里面。

在這里,我們用簡單的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的時候則是觸發(fā)trigger的觸發(fā)機制。在vue3,而trigger和track的話都是在我們effect.ts當中聲明的,那么接下來就來看看依賴收集和響應觸發(fā)究竟做了一些什么吧。
Effect
對于整個 effect 模塊,將其分為三個部分來去閱讀:
effect:副作用函數(shù)teack: 依賴收集,在proxy代理數(shù)據(jù)get時調用trigger: 觸發(fā)響應,在proxy代理數(shù)據(jù)發(fā)生變化的時候調用。
effect
通過一段實例來看下effect的使用,并且了解它主要參數(shù)是一個函數(shù)。在函數(shù)內部會幫你執(zhí)行一些副作用記錄和特性判斷。
effect(() => {
proxy.user = 1
})
復制代碼
來看看vue的effect干了什么?
在這里,首先判斷當前參數(shù)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進行存取當前執(zhí)行的effect,在執(zhí)行完后會將其進行出棧。同時替換activeEffect為新的棧頂。
而在effect執(zhí)行的過程中就會觸發(fā)proxy handle然后track和trigger兩個關鍵的函數(shù)。
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,拋開大多數(shù)代碼包袱,下面的代碼是不是清晰很多。
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來去實現(xiàn)的。
第一步先憑借當前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(響應觸發(fā))
在trigger的時候,做的事情其實就是觸發(fā)當前響應依賴的執(zhí)行。
首先,需要獲取當前key下所有渠道的deps,所以會看到有一個effects和add函數(shù), 做的事情非常的簡單,就是來判斷當前傳入的depsMap的屬性是否需要添加到effects里面,在這里的條件就是effect不能是當前的activeEffect和effect.allowRecurse,來確保當前set key的依賴都進行執(zhí)行。
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行為,然后執(zhí)行add方法將其將符合條件的effect添加到effects當中去,在這里@vue/reactivity做了很多數(shù)據(jù)就變異上的行為,如length變化。
然后根據(jù)不同的TriggerOpTypes進行depsMap的數(shù)據(jù)取出,最后放入effects。隨后通過run方法將當前的effect執(zhí)行,通過effects.forEach(run)進行執(zhí)行。
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來處理執(zhí)行,反之直接直接執(zhí)行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對普通類型的一個響應式數(shù)據(jù)聲明。而獲取ref的值需要通過ref.value的方式進行獲取,很多人以為ref就是一個簡單的reactive但其實不是。
在源碼中,ref最終是調用一個createRef的方法,在其內部返回了RefImpl的實例。它與Proxy不同的是,ref的依賴收集和響應觸發(fā)是在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觸發(fā)器。
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)
}
}
}
復制代碼
那么你現(xiàn)在應該知道:
proxy handle是reactive的原理,而ref的原理是getter/setter。在 get的時候都調用了track,set的時候都調用了triggereffect是數(shù)據(jù)響應的核心。
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為函數(shù)的時候,會將其賦值給與getter。
當getterOrOptions為對象的時候,會將set和get分別賦值給setter,getter。
隨后將其作為參數(shù)進行實例化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干了一些什么?
計算屬性的源碼,其實絕大多數(shù)是依賴前面對effect的一些理解。
首先,我們都知道,effect可以傳遞一個函數(shù)和一個對象options。
在這里將getter當作函數(shù)參數(shù)傳遞,也就是副作用,而在options當中配置了lazy和scheduler。
lazy表示effect并不會立即被執(zhí)行,而scheduler是在trigger中會判斷你是否傳入了scheduler,傳入后就執(zhí)行scheduler方法。
而在computed scheduler當中,會判斷當前的_dirty是否為false,如果是的話會把_dirty設置為true,且執(zhí)行trigger觸發(fā)響應。
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,如果是的話執(zhí)行緩存的effect并將其返回結果存放到_value,并執(zhí)行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 實現(xiàn)響應式狀態(tài)管理
總結
如果你使用vue的話強烈建議自己debug將這一塊看完,絕對會對你寫代碼有很大的幫助。vue3如火如荼,目前已經(jīng)有團隊作用于生產(chǎn)環(huán)境進行項目開發(fā),社區(qū)的生態(tài)也慢慢的發(fā)展起來。
@vue/reactivity的閱讀難度并不高,也有很多優(yōu)質的教程,有一定的工作基礎和代碼知識都能循序漸進的理解下來。我個人其實并不需要將其理解的滾瓜爛熟,理解每一行代碼的意思什么的,而是了解其核心思想,學習框架理念以及一些框架開發(fā)者代碼寫法的思路。這都是能夠借鑒并將其吸收成為自己的知識。
對于一個已經(jīng)轉到
React生態(tài)體系下的前端來說,讀Vue的源碼其實更多的是豐富自己在思維上的知識,而不是為了面試而去讀的。正如同你背書不是為了考試,而是學習知識。在現(xiàn)在的環(huán)境下,很難做到這些事情,靜下心來專心理解一件知識不如背幾篇面經(jīng)。
關注公眾號「前端Sharing」,持續(xù)為你推送精選好文。
