深度:1.1萬字解讀Vue3.0源碼響應(yīng)式系統(tǒng)

原文地址:https://hkc452.github.io/slamdunk-the-vue3/
作者:KC
effect 是響應(yīng)式系統(tǒng)的核心,而響應(yīng)式系統(tǒng)又是 vue3 中的核心,所以從 effect 開始講起。
首先看下面 effect 的傳參,fn 是回調(diào)函數(shù),options 是傳入的參數(shù)。
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
}
其中 option 的參數(shù)如下,都是屬于可選的。
參數(shù) & 含義
lazy 是否延遲觸發(fā) effect computed 是否為計算屬性 scheduler 調(diào)度函數(shù) onTrack 追蹤時觸發(fā) onTrigger 觸發(fā)回調(diào)時觸發(fā) onStop 停止監(jiān)聽時觸發(fā)
export interface ReactiveEffectOptions {
lazy?: boolean
computed?: boolean
scheduler?: (job: ReactiveEffect) => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
onStop?: () => void
}
分析完參數(shù)之后,繼續(xù)我們一開始的分析。當(dāng)我們調(diào)用 effect 時,首先判斷傳入的 fn 是否是 effect,如果是,取出原始值,然后調(diào)用 createReactiveEffect 創(chuàng)建 新的effect, 如果傳入的 option 中的 lazy 不為為 true,則立即調(diào)用我們剛剛創(chuàng)建的 effect, 最后返回剛剛創(chuàng)建的 effect。
那么
createReactiveEffect是怎樣是創(chuàng)建effect的呢?
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
我們先忽略 reactiveEffect,繼續(xù)看下面的掛載的屬性。
effect 掛載屬性 含義
id 自增id, 唯一標識effect
_isEffect 用于標識方法是否是effect
active effect 是否激活
raw 創(chuàng)建effect是傳入的fn
deps 持有當(dāng)前 effect 的dep 數(shù)組
options 創(chuàng)建effect是傳入的options
回到 reactiveEffect,如果 effect 不是激活狀態(tài),這種情況發(fā)生在我們調(diào)用了 effect 中的 stop 方法之后,那么先前沒有傳入調(diào)用 scheduler 函數(shù)的話,直接調(diào)用原始方法fn,否則直接返回。
那么處于激活狀態(tài)的 effect 要怎么進行處理呢?首先判斷是否當(dāng)前 effect 是否在 effectStack 當(dāng)中,如果在,則不進行調(diào)用,這個主要是為了避免死循環(huán)。拿下面測試用例來看
it('should avoid infinite loops with other effects', () => {
const nums = reactive({ num1: 0, num2: 1 })
const spy1 = jest.fn(() => (nums.num1 = nums.num2))
const spy2 = jest.fn(() => (nums.num2 = nums.num1))
effect(spy1)
effect(spy2)
expect(nums.num1).toBe(1)
expect(nums.num2).toBe(1)
expect(spy1).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledTimes(1)
nums.num2 = 4
expect(nums.num1).toBe(4)
expect(nums.num2).toBe(4)
expect(spy1).toHaveBeenCalledTimes(2)
expect(spy2).toHaveBeenCalledTimes(2)
nums.num1 = 10
expect(nums.num1).toBe(10)
expect(nums.num2).toBe(10)
expect(spy1).toHaveBeenCalledTimes(3)
expect(spy2).toHaveBeenCalledTimes(3)
})
如果不加 effectStack,會導(dǎo)致 num2 改變,觸發(fā)了 spy1, spy1 里面 num1 改變又觸發(fā)了 spy2, spy2 又會改變 num2,從而觸發(fā)了死循環(huán)。
接著是清除依賴,每次 effect 運行都會重新收集依賴, deps 是持有 effect 的依賴數(shù)組,其中里面的每個 dep 是對應(yīng)對象某個 key 的 全部依賴,我們在這里需要做的就是首先把 effect 從 dep 中刪除,最后把 deps 數(shù)組清空。
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
}
}
清除完依賴,就開始重新收集依賴。首先開啟依賴收集,把當(dāng)前 effect 放入 effectStack 中,然后講 activeEffect 設(shè)置為當(dāng)前的 effect,activeEffect 主要為了在收集依賴的時候使用(在下面會很快講到),然后調(diào)用 fn 并且返回值,當(dāng)這一切完成的時候,finally 階段,會把當(dāng)前 effect 彈出,恢復(fù)原來的收集依賴的狀態(tài),還有恢復(fù)原來的 activeEffect。
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
那 effect 是怎么收集依賴的呢?vue3 利用 proxy 劫持對象,在上面運行 effect 中讀取對象的時候,當(dāng)前對象的 key 的依賴 set集合 會把 effect 收集進去。
export function track(target: object, type: TrackOpTypes, key: unknown) {
...
}
vue3 在 reactive 中觸發(fā) track 函數(shù),reactive 會在單獨的章節(jié)講。觸發(fā) track 的參數(shù)中,object 表示觸發(fā) track 的對象, type 代表觸發(fā) track 類型,而 key 則是 觸發(fā) track 的 object 的 key。在下面可以看到三種類型的讀取對象會觸發(fā) track,分別是 get、 has、 iterate。
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
回到 track 內(nèi)部,如果 shouldTrack 為 false 或者 activeEffect 為空,則不進行依賴收集。接著 targetMap 里面有沒有該對象,沒有新建 map,然后再看這個 map 有沒有這個對象的對應(yīng) key 的 依賴 set 集合,沒有則新建一個。如果對象對應(yīng)的 key 的 依賴 set 集合也沒有當(dāng)前 activeEffect, 則把 activeEffect 加到 set 里面,同時把 當(dāng)前 set 塞到 activeEffect 的 deps 數(shù)組。最后如果是開發(fā)環(huán)境而且傳入了 onTrack 函數(shù),則觸發(fā) onTrack。所以 deps 就是 effect 中所依賴的 key 對應(yīng)的 set 集合數(shù)組, 畢竟一般來說,effect 中不止依賴一個對象或者不止依賴一個對象的一個key,而且 一個對象可以能不止被一個 effect 使用,所以是 set 集合數(shù)組。
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
})
}
}
依賴都收集完畢了,接下來就是觸發(fā)依賴。如果 targetMap 為空,說明這個對象沒有被追蹤,直接return。
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
}
...
}
其中觸發(fā)的 type, 包括了 set、add、delete 和 clear。
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
接下來對 key 收集的依賴進行分組,computedRunners 具有更高的優(yōu)先級,會觸發(fā)下游的 effects 重新收集依賴,
const effects = new Set
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}
下面根據(jù)觸發(fā) key 類型的不同進行 effect 的處理。如果是 clear 類型,則觸發(fā)這個對象所有的 effect。如果 key 是 length , 而且 target 是數(shù)組,則會觸發(fā) key 為 length 的 effects ,以及 key 大于等于新 length的 effects, 因為這些此時數(shù)組長度變化了。
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)
}
})
}
下面則是對正常的新增、修改、刪除進行 effect 的分組, isAddOrDelete 表示新增 或者不是數(shù)組的刪除,這為了對迭代 key的 effect 進行觸發(fā),如果 isAddOrDelete 為 true 或者是 map 對象的設(shè)值,則觸發(fā) isArray(target) ? 'length' : ITERATE_KEY 的 effect ,如果 isAddOrDelete 為 true 且 對象為 map, 則觸發(fā) MAP_KEY_ITERATE_KEY 的 effect
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
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
最后是運行 effect, 像上面所說的,computed effects 會優(yōu)先運行,因為 computed effects 在運行過程中,第一次會觸發(fā)上游把cumputed effect收集進去,再把下游 effect 收集起來。
還有一點,就是 effect.options.scheduler,如果傳入了調(diào)度函數(shù),則通過 scheduler 函數(shù)去運行 effect, 但是 scheduler 里面可能不一定使用了 effect,例如 computed 里面,因為 computed 是延遲運行 effect, 這個會在講 computed 的時候再講。
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
可以發(fā)現(xiàn),不管是 track 還是 trigger, 都會導(dǎo)致 effect 重新運行去收集依賴。
最后再講一個 stop 方法,當(dāng)我們調(diào)用 stop 方法后,會清空其他對象對 effect 的依賴,同時調(diào)用 onStop 回調(diào),最后將 effect 的激活狀態(tài)設(shè)置為 false
export function stop(effect: ReactiveEffect) {
if (effect.active) {
cleanup(effect)
if (effect.options.onStop) {
effect.options.onStop()
}
effect.active = false
}
}
這樣當(dāng)再一次調(diào)用 effect 的時候,不會進行依賴的重新收集,而且沒有調(diào)度函數(shù),就直接返回原始的 fn 的運行結(jié)果,否則直接返回 undefined。
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
reactive 是 vue3 中對數(shù)據(jù)進行劫持的核心,主要是利用了 Proxy 進行劫持,相比于 Object.defineproperty 能夠劫持的類型和范圍都更好,再也不用像 vue2 中那樣對數(shù)組進行類似 hack 方式的劫持了。
下面快速看看 vue3 是怎么劫持。首先看看這個對象是是不是 __v_isReadonly 只讀的,這個枚舉在后面進行講述,如果是,直接返回,否者調(diào)用 createReactiveObject 進行創(chuàng)建。
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target).__v_isReadonly) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
createReactiveObject 中,有個四個參數(shù),target 就是我們需要傳入的對象,isReadonly 表示要創(chuàng)建的代理是不是只可讀的,baseHandlers 是對進行基本類型的劫持,即 [Object,Array] ,collectionHandlers 是對集合類型的劫持, 即 [Set, Map, WeakMap, WeakSet]。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
return target
}
// target already has corresponding Proxy
if (
hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
) {
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target
}
const observed = new Proxy(
target,
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
def(
target,
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
observed
)
return observed
}
如果我們傳入是 target 不是object,直接返回。而如果 target 已經(jīng)是個 proxy ,而且不是要求這個proxy 是已讀的,但這個 proxy 是個響應(yīng)式的,則直接返回這個 target。什么意思呢?我們創(chuàng)建的 proxy 有兩種類型,一種是響應(yīng)式的,另外一種是只讀的。
而如果我們傳入的 target 上面有掛載了響應(yīng)式的 proxy,則直接返回上面掛載的 proxy 。
如果上面都不滿足,則需要檢查一下我們傳進去的 target 是否可以進行劫持觀察,如果 target 上面掛載了 __v_skip 屬性 為 true 或者 不是我們再在上面講參數(shù)時候講的六種類型,或者 對象被freeze 了,還是不能進行劫持。
const canObserve = (value: Target): boolean => {
return (
!value.__v_skip &&
isObservableType(toRawType(value)) &&
!Object.isFrozen(value)
)
}
如果上面條件滿足,則進行劫持,可以看到我們會根據(jù) target 類型的不同進行不同的 handler,最后根據(jù)把 observed 掛載到原對象上,同時返回 observed。
const observed = new Proxy(
target,
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
def(
target,
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
observed
)
return observed
現(xiàn)在繼續(xù)講講上面 ReactiveFlags 枚舉,skip 用于標記對象不可以進行代理,可以用于 創(chuàng)建 component 的時候,把options 進行 markRaw,isReactive 和 isReadonly 都是由 proxy 劫持返回值,表示 proxy 的屬性,raw 是 proxy 上面的 原始target ,reactive 和 readonly 是掛載在 target 上面的 proxy
export const enum ReactiveFlags {
skip = '__v_skip',
isReactive = '__v_isReactive',
isReadonly = '__v_isReadonly',
raw = '__v_raw',
reactive = '__v_reactive',
readonly = '__v_readonly'
}
再講講可以創(chuàng)建的四種 proxy, 分別是reactive、 shallowReactive 、readonly 和 shallowReadonly。其實從字面意思就可以看出他們的區(qū)別了。具體細節(jié)會在 collectionHandlers 和 baseHandlers 進行講解
baseHandlers 中主要包含四種 handler, mutableHandlers、readonlyHandlers、shallowReactiveHandlers、 shallowReadonlyHandlers。這里先介紹 mutableHandlers, 因為其他三種 handler 也算是 mutableHandlers 的變形版本。
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
從 mdn 上面可以看到,
handler.get() 方法用于攔截對象的讀取屬性操作。 handler.set() 方法是設(shè)置屬性值操作的捕獲器。 handler.deleteProperty() 方法用于攔截對對象屬性的 delete 操作。 handler.has() 方法是針對 in 操作符的代理方法。 handler.ownKeys() 方法用于攔截 Object.getOwnPropertyNames() Object.getOwnPropertySymbols() Object.keys() for…in循環(huán) 從下面可以看到 ownKeys 觸發(fā)時,主要追蹤 ITERATE 操作,has 觸發(fā)時,追蹤 HAS 操作,而 deleteProperty 觸發(fā)時,我們要看看是否刪除成功以及刪除的 key 是否是對象自身擁有的。
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
}
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
接下來看看 set handler, set 函數(shù)通過 createSetter 工廠方法 進行創(chuàng)建,/#PURE/ 是為了 rollup tree shaking 的操作。
對于非 shallow , 如果原來的對象不是數(shù)組, 舊值是 ref,新值不是 ref,則讓新的值 賦值給 ref.value , 讓 ref 去決定 trigger,這里不展開,ref 會在ref 章節(jié)展開。如果是 shallow ,管它三七二十一呢。
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
...
return result
}
}
接下來進行設(shè)置,需要注意的是,如果 target 是在原型鏈的值,那么 Reflect.set(target, key, value, receiver) 的設(shè)值值設(shè)置起作用的是 receiver 而不是 target,這也是什么在這種情況下不要觸發(fā) trigger 的原因。
那么在 target === toRaw(receiver) 時,如果原來 target 上面有 key, 則觸發(fā) SET 操作,否則觸發(fā) ADD 操作。
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
接下來說說 get 操作,get 有四種,我們先拿其中一種說說。
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
...
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
return res
}
if (shallow) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
}
if (isRef(res)) {
if (targetIsArray) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays.
return res.value
}
}
!isReadonly && track(target, TrackOpTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
首先如果 key 是 ReactiveFlags, 直接返回值,ReactiveFlags 的枚舉值在 reactive 中講過。
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (key === ReactiveFlags.raw) {
return target
}
而如果 target 是數(shù)組,而且調(diào)用了 ['includes', 'indexOf', 'lastIndexOf'] 這三個方法,則調(diào)用 arrayInstrumentations 進行獲取值,
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
arrayInstrumentations 中會觸發(fā)數(shù)組每一項值得 GET 追蹤,因為 一旦數(shù)組的變了,方法的返回值也會變,所以需要全部追蹤。對于 args 參數(shù),如果第一次調(diào)用返回失敗,會嘗試將 args 進行 toRaw 再調(diào)用一次。
const arrayInstrumentations: Record<string, Function> = {}
;['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayInstrumentations[key] = function(...args: any[]): any {
const arr = toRaw(this) as any
for (let i = 0, l = (this as any).length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
如果 key 是 Symbol ,而且也是 ecma 中 Symbol 內(nèi)置的 key 或者 key 是 獲取對象上面的原型,則直接返回 res 值。
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key) || key === 'proto') { return res }
而如果是 shallow 為 true,說明而且不是只讀的,則追蹤 GET 追蹤,這里可以看出,只讀不會進行追蹤。
if (shallow) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
}
接下來都是針對非 shallow的。如果返回值是 ref,且 target 是數(shù)組,在非可讀的情況下,進行 Get 的 Track 操作,對于如果 target 是對象,則直接返回 ref.value,但是不會在這里觸發(fā) Get 操作,而是由 ref 內(nèi)部進行 track。
if (isRef(res)) {
if (targetIsArray) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays.
return res.value
}
}
對于非只讀,我們還要根據(jù) key 進行 Track。而對于返回值,如果是對象,我們還要進行一層 wrap, 但這層是 lazy 的,也就是只有我們讀取到 key 的時候,才會讀下面的 值進行 reactive 包裝,這樣可以避免出現(xiàn)循環(huán)依賴而導(dǎo)致的錯誤,因為這樣就算里面有循環(huán)依賴也不怕,反正是延遲取值,而不會導(dǎo)致棧溢出。
!isReadonly && track(target, TrackOpTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
這就是 mutableHandlers ,而對于 readonlyHandlers,我們可以看出首先不允許任何 set、 deleteProperty 操作,然后對于 get,我們剛才也知道,不會進行 track 操作。剩下兩個 shallowGet 和 shallowReadonlyGet,就不在講了。
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
has,
ownKeys,
set(target, key) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target, key) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
}
collectionHandlers 主要是對 set、map、weakSet、weakMap 四種類型的對象進行劫持。主要有下面三種類型的 handler,當(dāng)然照舊,我們拿其中的 mutableCollectionHandlers 進行講解。剩余兩種結(jié)合理解。
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(false, false)
}
export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(false, false)(false, true)
}
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(true, false)
}
mutableCollectionHandlers 主要是對 collection 的方法進行劫持,所以主要是對 get 方法進行代理,接下來對 createInstrumentationGetter(false, false) 進行研究。
instrumentations 是代理 get 訪問的 handler,當(dāng)然如果我們訪問的 key 是 ReactiveFlags,直接返回存儲的值,否則如果訪問的 key 在 instrumentations 上,在由 instrumentations 進行處理。
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (key === ReactiveFlags.raw) {
return target
}
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
接下來看看 mutableInstrumentations ,可以看到 mutableInstrumentations 對常見集合的增刪改查以及 迭代方法進行了代理,我們就順著上面的 key 怎么進行攔截的。注意 this: MapTypes 是 ts 上對 this 類型進行標注
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, toReactive)
},
get size() {
return size((this as unknown) as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false)
}
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
readonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
false
)
shallowInstrumentations[method as string] = createIterableMethod(
method,
true,
true
)
})
get 方法 首先獲取 target ,對 target 進行 toRaw, 這個會被 createInstrumentationGetter 中的 proxy 攔截返回原始的 target,然后對 key 也進行一次 toRaw, 如果兩者不一樣,說明 key 也是 reative 的, 對 key 和 rawkey 都進行 track ,然后調(diào)用 target 原型上面的 has 方法,如果 key 為 true ,調(diào)用 get 獲取值,同時對值進行 wrap ,對于 mutableInstrumentations 而言,就是 toReactive。
function get(
target: MapTypes,
key: unknown,
wrap: typeof toReactive | typeof toReadonly | typeof toShallow
) {
target = toRaw(target)
const rawKey = toRaw(key)
if (key !== rawKey) {
track(target, TrackOpTypes.GET, key)
}
track(target, TrackOpTypes.GET, rawKey)
const { has, get } = getProto(target)
if (has.call(target, key)) {
return wrap(get.call(target, key))
} else if (has.call(target, rawKey)) {
return wrap(get.call(target, rawKey))
}
}
has 方法 跟 get 方法差不多,也是對 key 和 rawkey 進行 track。
function has(this: CollectionTypes, key: unknown): boolean {
const target = toRaw(this)
const rawKey = toRaw(key)
if (key !== rawKey) {
track(target, TrackOpTypes.HAS, key)
}
track(target, TrackOpTypes.HAS, rawKey)
const has = getProto(target).has
return has.call(target, key) || has.call(target, rawKey)
}
size 和 add 方法 size 最要是返回集合的大小,調(diào)用原型上的 size 方法,同時觸發(fā) ITERATE 類型的 track,而 add 方法添加進去之前要判斷原本是否已經(jīng)存在了,如果存在,則不會觸發(fā) ADD 類型的 trigger。
function size(target: IterableCollections) {
target = toRaw(target)
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(getProto(target), 'size', target)
}
function add(this: SetTypes, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
const result = proto.add.call(target, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, value, value)
}
return result
}
set 方法
set 方法是針對 map 類型的,從 this 的類型我們就可以看出來了, 同樣這里我們也會對 key 做兩個校驗,第一,是看看現(xiàn)在 map 上面有沒有存在同名的 key,來決定是觸發(fā) SET 還是 ADD 的 trigger, 第二,對于開發(fā)環(huán)境,會進行 checkIdentityKeys 檢查
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const { has, get, set } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
const result = set.call(target, key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
checkIdentityKeys 就是為了檢查目標對象上面,是不是同時存在 rawkey 和 key,因為這樣可能會數(shù)據(jù)不一致。
function checkIdentityKeys(
target: CollectionTypes,
has: (key: unknown) => boolean,
key: unknown
) {
const rawKey = toRaw(key)
if (rawKey !== key && has.call(target, rawKey)) {
const type = toRawType(target)
console.warn(
`Reactive ${type} contains both the raw and reactive ` +
`versions of the same object${type === `Map` ? `as keys` : ``}, ` +
`which can lead to inconsistencies. ` +
`Avoid differentiating between the raw and reactive versions ` +
`of an object and only use the reactive version if possible.`
)
}
}
deleteEntry 和 clear 方法 deleteEntry 主要是為了觸發(fā) DELETE trigger ,流程跟上面 set 方法差不多,而 clear 方法主要是觸發(fā) CLEAR track,但是里面做了一個防御性的操作,就是如果集合的長度已經(jīng)為0,則調(diào)用 clear 方法不會觸發(fā) trigger。
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get, delete: del } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = del.call(target, key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? target instanceof Map
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = getProto(target).clear.call(target)
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
forEach 方法 在調(diào)用 froEach 方法的時候會觸發(fā) ITERATE 類型的 track,需要注意 Size 方法也會同樣類型的 track,畢竟集合整體的變化會導(dǎo)致整個兩個方法的輸出不一樣。順帶提一句,還記得我們的 effect 時候的 trigger 嗎,對于 SET | ADD | DELETE 等類似的操作,因為會導(dǎo)致集合值得變化,所以也會觸發(fā) ITERATE_KEY 或則 MAP_KEY_ITERATE_KEY 的 effect 重新收集依賴。
在調(diào)用原型上的 forEach 進行循環(huán)的時候,會對 key 和 value 都進行一層 wrap,對于我們來說,就是 reactive。
function createForEach(isReadonly: boolean, shallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown
) {
const observed = this
const target = toRaw(observed)
const wrap = isReadonly ? toReadonly : shallow ? toShallow : toReactive
!isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
// important: create sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
function wrappedCallback(value: unknown, key: unknown) {
return callback.call(thisArg, wrap(value), wrap(key), observed)
}
return getProto(target).forEach.call(target, wrappedCallback)
}
}
createIterableMethod 方法 主要是對集合中的迭代進行代理,['keys', 'values', 'entries', Symbol.iterator] 主要是這四個方法。
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
readonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
false
)
shallowInstrumentations[method as string] = createIterableMethod(
method,
true,
true
)
})
可以看到,這個方法也會觸發(fā) TrackOpTypes.ITERATE 類型的 track,同樣也會在遍歷的時候?qū)χ颠M行 wrap,需要主要的是,這個方法主要是 iterator protocol 進行一個 polyfill, 所以需要實現(xiàn)同樣的接口方便外部進行迭代。
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
shallow: boolean
) {
return function(this: IterableCollections, ...args: unknown[]) {
const target = toRaw(this)
const isMap = target instanceof Map
const isPair = method === 'entries' || (method === Symbol.iterator && isMap)
const isKeyOnly = method === 'keys' && isMap
const innerIterator = getProto(target)[method].apply(target, args)
const wrap = isReadonly ? toReadonly : shallow ? toShallow : toReactive
!isReadonly &&
track(
target,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
// return a wrapped iterator which returns observed versions of the
// values emitted from the real iterator
return {
// iterator protocol
next() {
const { value, done } = innerIterator.next()
return done
? { value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// iterable protocol
[Symbol.iterator]() {
return this
}
}
}
}
總的來說對集合的代理,就是對集合方法的代理,在集合方法的執(zhí)行的時候,進行不同類型的 key 的 track 或者 trigger。
ref 其實就是 reactive 包了一層,讀取值要要通過 ref.value 進行讀取,同時進行 track ,而設(shè)置值的時候,也會先判斷相對于舊值是否有變化,有變化才進行設(shè)置,以及 trigger。話不多說,下面就進行 ref 的分析。
通過 createRef 創(chuàng)建 ref,如果傳入的 rawValue 本身就是一個 ref 的話,直接返回。
而如果 shallow 為 false, 直接讓 ref.value 等于 value,否則對 rawValue 進行 convert 轉(zhuǎn)化成 reactive。可以看到 __v_isRef 標識 一個對象是否是 ref,讀取 value 觸發(fā) track,設(shè)置 value 而且 newVal 的 toRaw 跟 原先的 rawValue 不一致,則進行設(shè)置,同樣對于非 shallow 也進行 convert。
export function ref(value?: unknown) {
return createRef(value)
}
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
let value = shallow ? rawValue : convert(rawValue)
const r = {
__v_isRef: true,
get value() {
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
trigger(
r,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
}
return r
}
triggerRef 手動觸發(fā) trigger ,對 shallowRef 可以由調(diào)用者手動觸發(fā)。unref 則是反向操作,取出 ref 中的 value 值。
export function triggerRef(ref: Ref) {
trigger(
ref,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: ref.value } : void 0
)
}
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
return isRef(ref) ? (ref.value as any) : ref
}
toRefs 是將一個 reactive 對象或者 readonly 轉(zhuǎn)化成 一個個 refs 對象,這個可以從 toRef 方法可以看出。
export function toRefs<T extends object>(object: T): ToRefs<T> {
if (__DEV__ && !isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
return {
__v_isRef: true,
get value(): any {
return object[key]
},
set value(newVal) {
object[key] = newVal
}
} as any
}
需要提到 baseHandlers 一點的是,對于非 shallow 模式中,對于 target 不是數(shù)組,會直接拿 ref.value 的值,而不是 ref。
if (isRef(res)) {
if (targetIsArray) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays.
return res.value
}
}
而 set 中,如果對于 target 是對象,oldValue 是 ref, value 不是 ref,直接把 vlaue 設(shè)置給 oldValue.value
if (!shallow) {
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
}
需要注意的是, ref 還支持自定義 ref,就是又調(diào)用者手動去觸發(fā) track 或者 trigger,就是通過工廠模式生成我們的 ref 的 get 和 set
export type CustomRefFactory<T> = (
track: () => void,
trigger: () => void
) => {
get: () => T
set: (value: T) => void
}
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
const { get, set } = factory(
() => track(r, TrackOpTypes.GET, 'value'),
() => trigger(r, TriggerOpTypes.SET, 'value')
)
const r = {
__v_isRef: true,
get value() {
return get()
},
set value(v) {
set(v)
}
}
return r as any
}
這個用法,我們可以在測試用例找到,
const custom = customRef((track, trigger) => ({
get() {
track()
return value
},
set(newValue: number) {
value = newValue
_trigger = trigger
}
}))
computed 就是計算屬性,可能會依賴其他 reactive 的值,同時會延遲和緩存計算值,具體怎么操作。show the code。需要注意的是,computed 不一定有 set 操作,因為可能是只讀 computed。
首先我們會對傳入的 getterOrOptions 進行解析,如果是方法,說明是只讀 computed,否則從 getterOrOptions 解析出 get 和 set 方法。
緊接著,利用 getter 創(chuàng)建 runner effect,需要注意的 effect 的三個參數(shù),第一是 lazy ,表明內(nèi)部創(chuàng)建 effect 之后,不會立即執(zhí)行。第二是 coumputed, 表明 computed 上游依賴改變的時候,會優(yōu)先 trigger runner effect,而 runner 也不會在這時被執(zhí)行的,原因看第三。第三,我們知道,effect 傳入 scheduler 的時候, effect 會 trigger 的時候會調(diào)用 scheduler 而不是直接調(diào)用 effect。而在 computed 中,我們可以看到 trigger(computed, TriggerOpTypes.SET, 'value') 觸發(fā)依賴 computed 的 effect 被重新收集依賴。同時因為 computed 是緩存和延遲計算,所以在依賴 computed effect 重新收集的過程中,runner 會在第一次計算 value,以及重新讓 runner 被收集依賴。這也是為什么要 computed effect 的優(yōu)先級要高的原因,因為讓 依賴的 computed的 effect 重新收集依賴,以及讓 runner 最早進行依賴收集,這樣才能計算出最新的 computed 值。
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
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
}
let dirty = true
let value: T
let computed: ComputedRef<T>
const runner = effect(getter, {
lazy: true,
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => {
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})
computed = {
__v_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
track(computed, TrackOpTypes.GET, 'value')
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any
return computed
}
從上面可以看出,effect 有可能被多次調(diào)用,像下面中 value.foo++,會導(dǎo)致 effectFn 運行兩次,因為同時被 effectFn 同時被 effectFn 和 c1 依賴了。PS: 下面這個測試用例是自己寫的,不是 Vue 里面的。
it('should trigger once', () => {
const value = reactive({ foo: 0 })
const getter1 = jest.fn(() => value.foo)
const c1 = computed(getter1)
const effectFn = jest.fn(() => {
value.foo
c1.value
})
effect(effectFn)
expect(effectFn).toBe(1)
value.foo++
// 原本以為是 2
expect(effectFn).toHaveBeenCalledTimes(3)
})
對于 computed 暴露出來的 effect ,主要為了調(diào)用 effect 里面 stop 方法停止依賴收集。至此,響應(yīng)式模塊分析完畢。
