前言
感覺 reactivity 代碼比較少兵琳,看看能不能一篇寫個(gè)大概狂秘。
一般這種響應(yīng)式的套路,都是 reactive
躯肌、observable
者春、observe
、observer
之類的名字清女,vue2x 中相應(yīng)邏輯的關(guān)鍵字也差不多:defineReactive + observe + new Observer
先說(shuō)結(jié)論
如果用盡可能少的文字來(lái)描述 reactive 的工作流程钱烟,就是:
- 定義 Proxy
- mount 過(guò)程中觸發(fā) get,進(jìn)而觸發(fā) track
- 通過(guò) effect 方法設(shè)置 activeEffect
- 綁定當(dāng)前 dep(依賴) 和 activeEffect
- 數(shù)據(jù)更新的時(shí)候觸發(fā) set,找到 dep拴袭,并觸發(fā)與其綁定的 effect
對(duì)比 2x 版本的詳細(xì)流程
- init 過(guò)程中定義 dep
- init 過(guò)程中調(diào)用 defineProperty
- beforeMount 到 mounted 之間創(chuàng)建 watcher
- 第一次執(zhí)行 watcher 的 getter 渲染視圖
- 觸發(fā) getter传惠,進(jìn)行 dep 與 Watcher 的綁定
- 數(shù)據(jù)更新觸發(fā) set,調(diào)用收到影響 dep 上的 watcher(例如組件的 render稻扬、watch 函數(shù)之類的操作)
之所以把結(jié)論搬到了前面。羊瘩。泰佳。是因?yàn)楹?jiǎn)書的 md 可讀性略差,接下來(lái)去看源碼尘吗。逝她。。
Proxy
掃了一眼vue-next-master/packages/reactivity/src/reactive.ts
睬捶,感覺出現(xiàn)頻率最高的就是函數(shù) createReactiveObject
黔宛,但是函數(shù)本身并沒(méi)有被 export ,對(duì)外暴露的是(主要就看了 reactive):
-
reactive
a. vue 內(nèi)部:在 collectionHandlers 的 toReactive 和 baseHandlers 的 createGetter 中被調(diào)用
b. 兼容的 data 函數(shù):在 data 函數(shù)里面的內(nèi)容是由該方法進(jìn)行 Proxy 加工的
c. 兼容的類+裝飾器寫法:使用vue-class-component
直接掛在類里面的變量會(huì)通過(guò) proxyRefs (mountComponent => setupComponent => setupStatefulComponent => handleSetupResult => proxyRefs) 進(jìn)行 Proxy 的加工
d. 新版 setup 中:可以直接創(chuàng)建 Proxy擒贸,然后會(huì)走上述 c 的流程臀晃,區(qū)別就是最后一步不需要再用 Proxy 進(jìn)行加工,直接把當(dāng)前的 Proxy return 了出去直接掛在 instance 的 setupState 上 -
shallowReactive
a. vue 內(nèi)部:調(diào)用時(shí)機(jī)在 setupComponent 的 initProps 時(shí)介劫,會(huì)把 vNode 上的 rawProps 變成 Proxy 掛在 instance.props 上 -
readonly
a. vue 內(nèi)部:在 collectionHandlers 的 toReadonly 被調(diào)用
b. 對(duì)外作為只讀加工使用 -
shallowReadonly
a. vue 內(nèi)部:調(diào)用時(shí)機(jī)在 setupComponent 的 initProps 之后的 setupStatefulComponent徽惋,用于進(jìn)一步處理 instance.props 和 組件內(nèi)的 slots
這四個(gè)函數(shù),對(duì)應(yīng)封裝好的 readonly 狀態(tài)座韵、handlers 和 collectionHandlers险绘,外部只需要傳個(gè) target 進(jìn)來(lái)就可以調(diào)用 createReactiveObject 函數(shù),舉個(gè)例子:
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
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
)
}
然后看下 createReactiveObject 的源碼:
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[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
官方的注釋已經(jīng)很清晰了誉碴,再?gòu)?qiáng)行描述一下流程就是:
- 判斷是不是對(duì)象宦棺,非對(duì)象無(wú)法處理成響應(yīng)式
- 判斷是不是已經(jīng)是一個(gè) Proxy
- 判斷 readOnly 狀態(tài),去相應(yīng)的 Map 找是否已存在對(duì)應(yīng)的 Proxy
- 判斷是不是非法類型
- 在 Map 中存儲(chǔ) Proxy黔帕,并 return 出去
看完了取代 defineProperty 的 Proxy代咸,還需要找到 2x 版本的依賴收集和派發(fā)更新對(duì)應(yīng)的邏輯
依賴收集
依賴收集就是 vue 去收集在改變的時(shí)候需要觸發(fā)視圖更新的變量
在 2 版本,是在 defineProperty 之前創(chuàng)建了對(duì)應(yīng)的 Dep 實(shí)例成黄,然后在變量 get 的時(shí)候把 Dep 和 Watcher 建立聯(lián)系
先只看看最常用的 reactive 的實(shí)現(xiàn)邏輯吧:
createReactiveObject
最后兩個(gè)入?yún)?baseHandlers
與 collectionHandlers
侣背,直接讀起來(lái) collectionHandlers
就是我們想找的邏輯,而 reactive 函數(shù)傳入的則是 mutableCollectionHandlers
慨默。
但是在 createReactiveObject
里兩個(gè) handlers 的使用邏輯贩耐,是基于 target 的 targetType 來(lái)判斷的,代碼如下:
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) // ReactiveFlags.SKIP 就是 "__v_skip"
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON // 1
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION // 2
default:
return TargetType.INVALID // 0
}
}
// ...
const targetType = getTargetType(target)
// ...
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
然后你就發(fā)現(xiàn)我們應(yīng)該需要去看的是 baseHandlers
對(duì)應(yīng)的 mutableHandlers
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
const keyIsSymbol = isSymbol(key)
if (
keyIsSymbol
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key) // <= 重點(diǎn)在這里
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) { // 遞歸 reactive/readonly
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
track 就是依賴收集的操作厦取,看下 track 的代碼(/packages/reactivity/src/effect.ts
)
export 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
})
}
}
}
感覺和 2 版本有些相同的地方潮太,就是依然有 dep 關(guān)聯(lián) watcher,watcher 的 deps 下面掛著收集起來(lái)的 dep。
簡(jiǎn)單的描述一下全局變量 activeEffect
铡买,它是靠 createReactiveEffect
和 effect
方法(代碼在 packages/reactivity/src/effect.ts
里)來(lái)進(jìn)行賦值操作的更鲁,比如在 mountComponent 的 setupRenderEffect,就是在 instance 的 update 方法上奇钞,掛了一個(gè) effect(function componentEffect() {...})澡为,這里面就會(huì)有組件的渲染邏輯。
而我們?cè)谟|發(fā) getter 的 track 過(guò)程中景埃,等于把變量和具有重新渲染視圖能力的 activeEffect 綁定在了一起媒至,從而完成了依賴的收集,具體代碼如下
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
}
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._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
派發(fā)更新
派發(fā)更新就是 vue 收集到的那些變量在變動(dòng)時(shí)谷徙,如何促使視圖跟著發(fā)生變化
在 2 版本拒啰,是靠在變量 set 的時(shí)候,dep 的 notify 觸發(fā) Watcher 的一系列響應(yīng)
而上文已經(jīng)看完了最簡(jiǎn)單的 get 相關(guān)邏輯( get 流程內(nèi)的分支代碼以及 watch 等的實(shí)現(xiàn)方式都還沒(méi)去看)完慧,也就是在 reactive 執(zhí)行時(shí)谋旦,會(huì)把對(duì)應(yīng)的 activeEffect(內(nèi)置 instance.update) 藏在 get 中等待收集。剩下就是大致看一下 set 觸發(fā)后屈尼,如何調(diào)用 到 instance.update 從而更新視圖的册着。
const set = /*#__PURE__*/ createSetter();
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
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: 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)
}
}
return result
}
}
一眼就看到了 trigger
根據(jù)依賴收集的結(jié)果,現(xiàn)在需要去尋找的核心思路是脾歧,怎么在 targetMap 里找到當(dāng)前 target 對(duì)應(yīng)的 depsMap 指蚜,然后找到當(dāng)前 key 對(duì)應(yīng)的 dep,然后再在 dep(Set 結(jié)構(gòu))中讀取需要執(zhí)行的函數(shù)
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
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect))
}
}
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
const shouldTriggerIteration =
(type === TriggerOpTypes.ADD &&
(!isArray(target) || isIntegerKey(key))) ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
shouldTriggerIteration ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (shouldTriggerIteration && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
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()
}
}
effects.forEach(run)
}
這里主要就是識(shí)別一下 trigger 的 type涨椒,然后靠 add 收集 effect摊鸡,最后靠 run 來(lái)執(zhí)行收集起來(lái)的 effect,如果中間那一坨看著不太直觀蚕冬,下面是打包出來(lái)的 esm 代碼
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case "add" /* 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 "delete" /* DELETE */:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
break;
case "set" /* SET */:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY));
}
break;
}
擴(kuò)展閱讀(沒(méi)啥意義)
下面是擴(kuò)展閱讀免猾,沒(méi)啥太大意義,只是檢查除此以為沒(méi)有其他的依賴收集入口
前文還提到了囤热,使用 reactive 之后猎提,在組件初始化的時(shí)候,會(huì)走一遍(mountComponent => setupComponent => setupStatefulComponent => handleSetupResult => proxyRefs)這個(gè)流程旁蔼。更直白的講锨苏,我們這里只是在 setup,組件初始化的時(shí)候會(huì)執(zhí)行 handleSetupResult 來(lái)處理我們 setup 的結(jié)果
在 setupStatefulComponent
中創(chuàng)建了一個(gè)新的 Proxy棺聊,看注釋是說(shuō)一個(gè)公用的實(shí)例/渲染的 proxy
但是你追進(jìn)去就會(huì)發(fā)現(xiàn)伞租,這個(gè)只針對(duì) instance.ctx 的 proxy 只有在 dev
模式下,才會(huì)把 setup 內(nèi) reactive 加工的變量加到 ctx 上 (邏輯在 handleSetupResult
函數(shù)內(nèi)部限佩,exposePropsOnRenderContext
的調(diào)用)
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
//...
// 0. create render proxy property access cache
instance.accessCache = {}
// 1. create public instance / render proxy <= 這里看上去很重要
// also mark it raw so it's never observed
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup() <= 處理我們的 setup 函數(shù)
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
currentInstance = null
if (isPromise(setupResult)) {
// ... 省略了葵诈,你可以全當(dāng)這里也是在 handleSetupResult
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
handleSetupResult
的代碼:
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup returned an inline render function
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)) {
if (__DEV__ && isVNode(setupResult)) {
warn(
`setup() should not return VNodes directly - ` +
`return a render function instead.`
)
}
// setup returned bindings.
// assuming a render function compiled from template is present.
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
instance.devtoolsRawSetupState = setupResult
}
instance.setupState = proxyRefs(setupResult)
if (__DEV__) {
exposeSetupStateOnRenderContext(instance)
}
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult
}`
)
}
finishComponentSetup(instance, isSSR)
}
// dev only
export function exposeSetupStateOnRenderContext(
instance: ComponentInternalInstance
) {
const { ctx, setupState } = instance
Object.keys(toRaw(setupState)).forEach(key => {
if (key[0] === '$' || key[0] === '_') {
warn(
`setup() return property ${JSON.stringify(
key
)} should not start with "$" or "_" ` +
`which are reserved prefixes for Vue internals.`
)
return
}
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => setupState[key],
set: NOOP
})
})
}
看來(lái)并沒(méi)有遺漏什么重要的邏輯裸弦,所以我們就不繼續(xù)追這個(gè) proxy 了,有興趣的同學(xué)可以繼續(xù)追一下 PublicInstanceProxyHandlers 的代碼作喘,看看有沒(méi)有什么神奇的地方
擴(kuò)展閱讀結(jié)束
下次準(zhǔn)備看看什么是 Composition API 的理疙,了解下熱點(diǎn)。