「不容錯(cuò)過」2.7萬字手摸手解讀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(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
其中 option 的參數(shù)如下,都是屬于可選的。
參數(shù) & 含義
lazy 是否延遲觸發(fā) effect computed 是否為計(jì)算屬性 scheduler 調(diào)度函數(shù) onTrack 追蹤時(shí)觸發(fā) onTrigger 觸發(fā)回調(diào)時(shí)觸發(fā) onStop 停止監(jiān)聽時(shí)觸發(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 時(shí),首先判斷傳入的 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(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect {
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, 唯一標(biāo)識(shí)effect
_isEffect 用于標(biāo)識(shí)方法是否是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 要怎么進(jìn)行處理呢?首先判斷是否當(dāng)前 effect 是否在 effectStack 當(dāng)中,如果在,則不進(jìn)行調(diào)用,這個(gè)主要是為了避免死循環(huán)。拿下面測(cè)試用例來看
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,會(huì)導(dǎo)致 num2 改變,觸發(fā)了 spy1, spy1 里面 num1 改變又觸發(fā)了 spy2, spy2 又會(huì)改變 num2,從而觸發(fā)了死循環(huán)。
接著是清除依賴,每次 effect 運(yùn)行都會(huì)重新收集依賴, deps 是持有 effect 的依賴數(shù)組,其中里面的每個(gè) dep 是對(duì)應(yīng)對(duì)象某個(gè) key 的 全部依賴,我們?cè)谶@里需要做的就是首先把 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 主要為了在收集依賴的時(shí)候使用(在下面會(huì)很快講到),然后調(diào)用 fn 并且返回值,當(dāng)這一切完成的時(shí)候,finally 階段,會(huì)把當(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 劫持對(duì)象,在上面運(yùn)行 effect 中讀取對(duì)象的時(shí)候,當(dāng)前對(duì)象的 key 的依賴 set集合 會(huì)把 effect 收集進(jìn)去。
export function track(target: object, type: TrackOpTypes, key: unknown) {
...
}
vue3 在 reactive 中觸發(fā) track 函數(shù),reactive 會(huì)在單獨(dú)的章節(jié)講。觸發(fā) track 的參數(shù)中,object 表示觸發(fā) track 的對(duì)象, type 代表觸發(fā) track 類型,而 key 則是 觸發(fā) track 的 object 的 key。在下面可以看到三種類型的讀取對(duì)象會(huì)觸發(fā) track,分別是 get、 has、 iterate。
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
回到 track 內(nèi)部,如果 shouldTrack 為 false 或者 activeEffect 為空,則不進(jìn)行依賴收集。接著 targetMap 里面有沒有該對(duì)象,沒有新建 map,然后再看這個(gè) map 有沒有這個(gè)對(duì)象的對(duì)應(yīng) key 的 依賴 set 集合,沒有則新建一個(gè)。 如果對(duì)象對(duì)應(yīng)的 key 的 依賴 set 集合也沒有當(dāng)前 activeEffect, 則把 activeEffect 加到 set 里面,同時(shí)把 當(dāng)前 set 塞到 activeEffect 的 deps 數(shù)組。最后如果是開發(fā)環(huán)境而且傳入了 onTrack 函數(shù),則觸發(fā) onTrack。 所以 deps 就是 effect 中所依賴的 key 對(duì)應(yīng)的 set 集合數(shù)組, 畢竟一般來說,effect 中不止依賴一個(gè)對(duì)象或者不止依賴一個(gè)對(duì)象的一個(gè)key,而且 一個(gè)對(duì)象可以能不止被一個(gè) 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 為空,說明這個(gè)對(duì)象沒有被追蹤,直接return。
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map | Set
) {
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'
}
接下來對(duì) key 收集的依賴進(jìn)行分組,computedRunners 具有更高的優(yōu)先級(jí),會(huì)觸發(fā)下游的 effects 重新收集依賴,
const effects = new Set
const add = (effectsToAdd: Set | 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 類型的不同進(jìn)行 effect 的處理。如果是 clear 類型,則觸發(fā)這個(gè)對(duì)象所有的 effect。如果 key 是 length , 而且 target 是數(shù)組,則會(huì)觸發(fā) key 為 length 的 effects ,以及 key 大于等于新 length的 effects, 因?yàn)檫@些此時(shí)數(shù)組長(zhǎng)度變化了。
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)
}
})
}
下面則是對(duì)正常的新增、修改、刪除進(jìn)行 effect 的分組, isAddOrDelete 表示新增 或者不是數(shù)組的刪除,這為了對(duì)迭代 key的 effect 進(jìn)行觸發(fā),如果 isAddOrDelete 為 true 或者是 map 對(duì)象的設(shè)值,則觸發(fā) isArray(target) ? 'length' : ITERATE_KEY 的 effect ,如果 isAddOrDelete 為 true 且 對(duì)象為 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))
}
}
最后是運(yùn)行 effect, 像上面所說的,computed effects 會(huì)優(yōu)先運(yùn)行,因?yàn)?computed effects 在運(yùn)行過程中,第一次會(huì)觸發(fā)上游把cumputed effect收集進(jìn)去,再把下游 effect 收集起來。
還有一點(diǎn),就是 effect.options.scheduler,如果傳入了調(diào)度函數(shù),則通過 scheduler 函數(shù)去運(yùn)行 effect, 但是 scheduler 里面可能不一定使用了 effect,例如 computed 里面,因?yàn)?computed 是延遲運(yùn)行 effect, 這個(gè)會(huì)在講 computed 的時(shí)候再講。
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, 都會(huì)導(dǎo)致 effect 重新運(yùn)行去收集依賴。
最后再講一個(gè) stop 方法,當(dāng)我們調(diào)用 stop 方法后,會(huì)清空其他對(duì)象對(duì) effect 的依賴,同時(shí)調(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 的時(shí)候,不會(huì)進(jìn)行依賴的重新收集,而且沒有調(diào)度函數(shù),就直接返回原始的 fn 的運(yùn)行結(jié)果,否則直接返回 undefined。
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
reactive 是 vue3 中對(duì)數(shù)據(jù)進(jìn)行劫持的核心,主要是利用了 Proxy 進(jìn)行劫持,相比于 Object.defineproperty 能夠劫持的類型和范圍都更好,再也不用像 vue2 中那樣對(duì)數(shù)組進(jìn)行類似 hack 方式的劫持了。
下面快速看看 vue3 是怎么劫持。首先看看這個(gè)對(duì)象是是不是 __v_isReadonly 只讀的,這個(gè)枚舉在后面進(jìn)行講述,如果是,直接返回,否者調(diào)用 createReactiveObject 進(jìn)行創(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 中,有個(gè)四個(gè)參數(shù),target 就是我們需要傳入的對(duì)象,isReadonly 表示要?jiǎng)?chuàng)建的代理是不是只可讀的,baseHandlers 是對(duì)進(jìn)行基本類型的劫持,即 [Object,Array] ,collectionHandlers 是對(duì)集合類型的劫持, 即 [Set, Map, WeakMap, WeakSet]。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler,
collectionHandlers: ProxyHandler
) {
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)是個(gè) proxy ,而且不是要求這個(gè)proxy 是已讀的,但這個(gè) proxy 是個(gè)響應(yīng)式的,則直接返回這個(gè) target。什么意思呢?我們創(chuàng)建的 proxy 有兩種類型,一種是響應(yīng)式的,另外一種是只讀的。
而如果我們傳入的 target 上面有掛載了響應(yīng)式的 proxy,則直接返回上面掛載的 proxy 。
如果上面都不滿足,則需要檢查一下我們傳進(jìn)去的 target 是否可以進(jìn)行劫持觀察,如果 target 上面掛載了 __v_skip 屬性 為 true 或者 不是我們?cè)僭谏厦嬷v參數(shù)時(shí)候講的六種類型,或者 對(duì)象被freeze 了,還是不能進(jìn)行劫持。
const canObserve = (value: Target): boolean => {
return (
!value.__v_skip &&
isObservableType(toRawType(value)) &&
!Object.isFrozen(value)
)
}
如果上面條件滿足,則進(jìn)行劫持,可以看到我們會(huì)根據(jù) target 類型的不同進(jìn)行不同的 handler,最后根據(jù)把 observed 掛載到原對(duì)象上,同時(shí)返回 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 用于標(biāo)記對(duì)象不可以進(jìn)行代理,可以用于 創(chuàng)建 component 的時(shí)候,把options 進(jìn)行 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。其實(shí)從字面意思就可以看出他們的區(qū)別了。具體細(xì)節(jié)會(huì)在 collectionHandlers 和 baseHandlers 進(jìn)行講解
baseHandlers 中主要包含四種 handler, mutableHandlers、readonlyHandlers、shallowReactiveHandlers、 shallowReadonlyHandlers。 這里先介紹 mutableHandlers, 因?yàn)槠渌N handler 也算是 mutableHandlers 的變形版本。
export const mutableHandlers: ProxyHandler從 mdn 上面可以看到,
handler.get() 方法用于攔截對(duì)象的讀取屬性操作。 handler.set() 方法是設(shè)置屬性值操作的捕獲器。 handler.deleteProperty() 方法用于攔截對(duì)對(duì)象屬性的 delete 操作。 handler.has() 方法是針對(duì) in 操作符的代理方法。 handler.ownKeys() 方法用于攔截 Object.getOwnPropertyNames() Object.getOwnPropertySymbols() Object.keys() for…in循環(huán) 從下面可以看到 ownKeys 觸發(fā)時(shí),主要追蹤 ITERATE 操作,has 觸發(fā)時(shí),追蹤 HAS 操作,而 deleteProperty 觸發(fā)時(shí),我們要看看是否刪除成功以及刪除的 key 是否是對(duì)象自身擁有的。
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 工廠方法 進(jìn)行創(chuàng)建,/#PURE/ 是為了 rollup tree shaking 的操作。
對(duì)于非 shallow , 如果原來的對(duì)象不是數(shù)組, 舊值是 ref,新值不是 ref,則讓新的值 賦值給 ref.value , 讓 ref 去決定 trigger,這里不展開,ref 會(huì)在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
}
}
接下來進(jìn)行設(shè)置,需要注意的是,如果 target 是在原型鏈的值,那么 Reflect.set(target, key, value, receiver) 的設(shè)值值設(shè)置起作用的是 receiver 而不是 target,這也是什么在這種情況下不要觸發(fā) trigger 的原因。
那么在 target === toRaw(receiver) 時(shí),如果原來 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'] 這三個(gè)方法,則調(diào)用 arrayInstrumentations 進(jìn)行獲取值,
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
arrayInstrumentations 中會(huì)觸發(fā)數(shù)組每一項(xiàng)值得 GET 追蹤,因?yàn)?一旦數(shù)組的變了,方法的返回值也會(huì)變,所以需要全部追蹤。對(duì)于 args 參數(shù),如果第一次調(diào)用返回失敗,會(huì)嘗試將 args 進(jìn)行 toRaw 再調(diào)用一次。
const arrayInstrumentations: Record = {}
;['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 是 獲取對(duì)象上面的原型,則直接返回 res 值。
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key) || key === 'proto') { return res }
而如果是 shallow 為 true,說明而且不是只讀的,則追蹤 GET 追蹤,這里可以看出,只讀不會(huì)進(jìn)行追蹤。
if (shallow) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
}
接下來都是針對(duì)非 shallow的。 如果返回值是 ref,且 target 是數(shù)組,在非可讀的情況下,進(jìn)行 Get 的 Track 操作,對(duì)于如果 target 是對(duì)象,則直接返回 ref.value,但是不會(huì)在這里觸發(fā) Get 操作,而是由 ref 內(nèi)部進(jìn)行 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
}
}
對(duì)于非只讀,我們還要根據(jù) key 進(jìn)行 Track。而對(duì)于返回值,如果是對(duì)象,我們還要進(jìn)行一層 wrap, 但這層是 lazy 的,也就是只有我們讀取到 key 的時(shí)候,才會(huì)讀下面的 值進(jìn)行 reactive 包裝,這樣可以避免出現(xiàn)循環(huán)依賴而導(dǎo)致的錯(cuò)誤,因?yàn)檫@樣就算里面有循環(huán)依賴也不怕,反正是延遲取值,而不會(huì)導(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 ,而對(duì)于 readonlyHandlers,我們可以看出首先不允許任何 set、 deleteProperty 操作,然后對(duì)于 get,我們剛才也知道,不會(huì)進(jìn)行 track 操作。剩下兩個(gè) shallowGet 和 shallowReadonlyGet,就不在講了。
export const readonlyHandlers: ProxyHandlercollectionHandlers 主要是對(duì) set、map、weakSet、weakMap 四種類型的對(duì)象進(jìn)行劫持。 主要有下面三種類型的 handler,當(dāng)然照舊,我們拿其中的 mutableCollectionHandlers 進(jìn)行講解。剩余兩種結(jié)合理解。
export const mutableCollectionHandlers: ProxyHandler = {
get: createInstrumentationGetter(false, false)
}
export const shallowCollectionHandlers: ProxyHandler = {
get: createInstrumentationGetter(false, false)(false, true)
}
export const readonlyCollectionHandlers: ProxyHandler = {
get: createInstrumentationGetter(true, false)
}
mutableCollectionHandlers 主要是對(duì) collection 的方法進(jìn)行劫持,所以主要是對(duì) get 方法進(jìn)行代理,接下來對(duì) createInstrumentationGetter(false, false) 進(jìn)行研究。
instrumentations 是代理 get 訪問的 handler,當(dāng)然如果我們?cè)L問的 key 是 ReactiveFlags,直接返回存儲(chǔ)的值,否則如果訪問的 key 在 instrumentations 上,在由 instrumentations 進(jìn)行處理。
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 對(duì)常見集合的增刪改查以及 迭代方法進(jìn)行了代理,我們就順著上面的 key 怎么進(jìn)行攔截的。注意 this: MapTypes 是 ts 上對(duì) this 類型進(jìn)行標(biāo)注
const mutableInstrumentations: Record = {
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 ,對(duì) target 進(jìn)行 toRaw, 這個(gè)會(huì)被 createInstrumentationGetter 中的 proxy 攔截返回原始的 target,然后對(duì) key 也進(jìn)行一次 toRaw, 如果兩者不一樣,說明 key 也是 reative 的, 對(duì) key 和 rawkey 都進(jìn)行 track ,然后調(diào)用 target 原型上面的 has 方法,如果 key 為 true ,調(diào)用 get 獲取值,同時(shí)對(duì)值進(jìn)行 wrap ,對(duì)于 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 方法差不多,也是對(duì) key 和 rawkey 進(jìn)行 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 方法,同時(shí)觸發(fā) ITERATE 類型的 track,而 add 方法添加進(jìn)去之前要判斷原本是否已經(jīng)存在了,如果存在,則不會(huì)觸發(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 方法是針對(duì) map 類型的,從 this 的類型我們就可以看出來了, 同樣這里我們也會(huì)對(duì) key 做兩個(gè)校驗(yàn),第一,是看看現(xiàn)在 map 上面有沒有存在同名的 key,來決定是觸發(fā) SET 還是 ADD 的 trigger, 第二,對(duì)于開發(fā)環(huán)境,會(huì)進(jì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 就是為了檢查目標(biāo)對(duì)象上面,是不是同時(shí)存在 rawkey 和 key,因?yàn)檫@樣可能會(huì)數(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,但是里面做了一個(gè)防御性的操作,就是如果集合的長(zhǎng)度已經(jīng)為0,則調(diào)用 clear 方法不會(huì)觸發(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 方法的時(shí)候會(huì)觸發(fā) ITERATE 類型的 track,需要注意 Size 方法也會(huì)同樣類型的 track,畢竟集合整體的變化會(huì)導(dǎo)致整個(gè)兩個(gè)方法的輸出不一樣。順帶提一句,還記得我們的 effect 時(shí)候的 trigger 嗎,對(duì)于 SET | ADD | DELETE 等類似的操作,因?yàn)闀?huì)導(dǎo)致集合值得變化,所以也會(huì)觸發(fā) ITERATE_KEY 或則 MAP_KEY_ITERATE_KEY 的 effect 重新收集依賴。
在調(diào)用原型上的 forEach 進(jìn)行循環(huán)的時(shí)候,會(huì)對(duì) key 和 value 都進(jìn)行一層 wrap,對(duì)于我們來說,就是 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 方法 主要是對(duì)集合中的迭代進(jìn)行代理,['keys', 'values', 'entries', Symbol.iterator] 主要是這四個(gè)方法。
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
)
})
可以看到,這個(gè)方法也會(huì)觸發(fā) TrackOpTypes.ITERATE 類型的 track,同樣也會(huì)在遍歷的時(shí)候?qū)χ颠M(jìn)行 wrap,需要主要的是,這個(gè)方法主要是 iterator protocol 進(jìn)行一個(gè) polyfill, 所以需要實(shí)現(xiàn)同樣的接口方便外部進(jì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
}
}
}
}
總的來說對(duì)集合的代理,就是對(duì)集合方法的代理,在集合方法的執(zhí)行的時(shí)候,進(jìn)行不同類型的 key 的 track 或者 trigger。
ref 其實(shí)就是 reactive 包了一層,讀取值要要通過 ref.value 進(jìn)行讀取,同時(shí)進(jìn)行 track ,而設(shè)置值的時(shí)候,也會(huì)先判斷相對(duì)于舊值是否有變化,有變化才進(jìn)行設(shè)置,以及 trigger。話不多說,下面就進(jìn)行 ref 的分析。
通過 createRef 創(chuàng)建 ref,如果傳入的 rawValue 本身就是一個(gè) ref 的話,直接返回。
而如果 shallow 為 false, 直接讓 ref.value 等于 value,否則對(duì) rawValue 進(jìn)行 convert 轉(zhuǎn)化成 reactive??梢钥吹?__v_isRef 標(biāo)識(shí) 一個(gè)對(duì)象是否是 ref,讀取 value 觸發(fā) track,設(shè)置 value 而且 newVal 的 toRaw 跟 原先的 rawValue 不一致,則進(jìn)行設(shè)置,同樣對(duì)于非 shallow 也進(jìn)行 convert。
export function ref(value?: unknown) {
return createRef(value)
}
const convert = (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 手動(dòng)觸發(fā) trigger ,對(duì) shallowRef 可以由調(diào)用者手動(dòng)觸發(fā)。 unref 則是反向操作,取出 ref 中的 value 值。
export function triggerRef(ref: Ref) {
trigger(
ref,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: ref.value } : void 0
)
}
export function unref(ref: T): T extends Ref ? V : T {
return isRef(ref) ? (ref.value as any) : ref
}
toRefs 是將一個(gè) reactive 對(duì)象或者 readonly 轉(zhuǎn)化成 一個(gè)個(gè) refs 對(duì)象,這個(gè)可以從 toRef 方法可以看出。
export function toRefs(object: T): ToRefs {
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(
object: T,
key: K
): Ref {
return {
__v_isRef: true,
get value(): any {
return object[key]
},
set value(newVal) {
object[key] = newVal
}
} as any
}
需要提到 baseHandlers 一點(diǎn)的是,對(duì)于非 shallow 模式中,對(duì)于 target 不是數(shù)組,會(huì)直接拿 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 中,如果對(duì)于 target 是對(duì)象,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)用者手動(dòng)去觸發(fā) track 或者 trigger,就是通過工廠模式生成我們的 ref 的 get 和 set
export type CustomRefFactory = (
track: () => void,
trigger: () => void
) => {
get: () => T
set: (value: T) => void
}
export function customRef(factory: CustomRefFactory): Ref {
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
}
這個(gè)用法,我們可以在測(cè)試用例找到,
const custom = customRef((track, trigger) => ({
get() {
track()
return value
},
set(newValue: number) {
value = newValue
_trigger = trigger
}
}))
computed 就是計(jì)算屬性,可能會(huì)依賴其他 reactive 的值,同時(shí)會(huì)延遲和緩存計(jì)算值,具體怎么操作。show the code。需要注意的是,computed 不一定有 set 操作,因?yàn)榭赡苁侵蛔x computed。
首先我們會(huì)對(duì)傳入的 getterOrOptions 進(jìn)行解析,如果是方法,說明是只讀 computed,否則從 getterOrOptions 解析出 get 和 set 方法。
緊接著,利用 getter 創(chuàng)建 runner effect,需要注意的 effect 的三個(gè)參數(shù),第一是 lazy ,表明內(nèi)部創(chuàng)建 effect 之后,不會(huì)立即執(zhí)行。第二是 coumputed, 表明 computed 上游依賴改變的時(shí)候,會(huì)優(yōu)先 trigger runner effect,而 runner 也不會(huì)在這時(shí)被執(zhí)行的,原因看第三。第三,我們知道,effect 傳入 scheduler 的時(shí)候, effect 會(huì) trigger 的時(shí)候會(huì)調(diào)用 scheduler 而不是直接調(diào)用 effect。而在 computed 中,我們可以看到 trigger(computed, TriggerOpTypes.SET, 'value') 觸發(fā)依賴 computed 的 effect 被重新收集依賴。同時(shí)因?yàn)?computed 是緩存和延遲計(jì)算,所以在依賴 computed effect 重新收集的過程中,runner 會(huì)在第一次計(jì)算 value,以及重新讓 runner 被收集依賴。這也是為什么要 computed effect 的優(yōu)先級(jí)要高的原因,因?yàn)樽?依賴的 computed的 effect 重新收集依賴,以及讓 runner 最早進(jìn)行依賴收集,這樣才能計(jì)算出最新的 computed 值。
export function computed(
getterOrOptions: ComputedGetter | WritableComputedOptions
) {
let getter: ComputedGetter
let setter: ComputedSetter
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
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++,會(huì)導(dǎo)致 effectFn 運(yùn)行兩次,因?yàn)橥瑫r(shí)被 effectFn 同時(shí)被 effectFn 和 c1 依賴了。PS: 下面這個(gè)測(cè)試用例是自己寫的,不是 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)
})
對(duì)于 computed 暴露出來的 effect ,主要為了調(diào)用 effect 里面 stop 方法停止依賴收集。至此,響應(yīng)式模塊分析完畢。
