前言
感觉 reactivity 代码比较少,看看能不能一篇写个大概。
一般这种响应式的套路,都是 reactive
、observable
、observe
、observer
之类的名字,vue2x 中相应逻辑的关键字也差不多:defineReactive + observe + new Observer
先说结论
如果用尽可能少的文字来描述 reactive 的工作流程,就是:
- 定义 Proxy
- mount 过程中触发 get,进而触发 track
- 通过 effect 方法设置 activeEffect
- 绑定当前 dep(依赖) 和 activeEffect
- 数据更新的时候触发 set,找到 dep,并触发与其绑定的 effect
对比 2x 版本的详细流程
- init 过程中定义 dep
- init 过程中调用 defineProperty
- beforeMount 到 mounted 之间创建 watcher
- 第一次执行 watcher 的 getter 渲染视图
- 触发 getter,进行 dep 与 Watcher 的绑定
- 数据更新触发 set,调用收到影响 dep 上的 watcher(例如组件的 render、watch 函数之类的操作)
之所以把结论搬到了前面。。。是因为的 md 可读性略差,接下来去看源码。。。
Proxy
扫了一眼vue-next-master/packages/reactivity/src/reactive.ts
,感觉出现频率最高的就是函数 createReactiveObject
,但是函数本身并没有被 export ,对外暴露的是(主要就看了 reactive):
-
reactive
a. vue 内部:在 collectionHandlers 的 toReactive 和 baseHandlers 的 createGetter 中被调用
b. 兼容的 data 函数:在 data 函数里面的内容是由该方法进行 Proxy 加工的
c. 兼容的类+装饰器写法:使用vue-class-component
直接挂在类里面的变量会通过 proxyRefs (mountComponent => setupComponent => setupStatefulComponent => handleSetupResult => proxyRefs) 进行 Proxy 的加工
d. 新版 setup 中:可以直接创建 Proxy,然后会走上述 c 的流程,区别就是最后一步不需要再用 Proxy 进行加工,直接把当前的 Proxy return 了出去直接挂在 instance 的 setupState 上 -
shallowReactive
a. vue 内部:调用时机在 setupComponent 的 initProps 时,会把 vNode 上的 rawProps 变成 Proxy 挂在 instance.props 上 -
readonly
a. vue 内部:在 collectionHandlers 的 toReadonly 被调用
b. 对外作为只读加工使用 -
shallowReadonly
a. vue 内部:调用时机在 setupComponent 的 initProps 之后的 setupStatefulComponent,用于进一步处理 instance.props 和 组件内的 slots
这四个函数,对应封装好的 readonly 状态、handlers 和 collectionHandlers,外部只需要传个 target 进来就可以调用 createReactiveObject 函数,举个例子:
export function reactive(target: T): UnwrapNestedRefs
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,
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[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
}
官方的注释已经很清晰了,再强行描述一下流程就是:
- 判断是不是对象,非对象无法处理成响应式
- 判断是不是已经是一个 Proxy
- 判断 readOnly 状态,去相应的 Map 找是否已存在对应的 Proxy
- 判断是不是非法类型
- 在 Map 中存储 Proxy,并 return 出去
看完了取代 defineProperty 的 Proxy,还需要找到 2x 版本的依赖收集和派发更新对应的逻辑
依赖收集
依赖收集就是 vue 去收集在改变的时候需要触发视图更新的变量
在 2 版本,是在 defineProperty 之前创建了对应的 Dep 实例,然后在变量 get 的时候把 Dep 和 Watcher 建立联系
先只看看最常用的 reactive 的实现逻辑吧:
createReactiveObject
最后两个入参 baseHandlers
与 collectionHandlers
,直接读起来 collectionHandlers
就是我们想找的逻辑,而 reactive 函数传入的则是 mutableCollectionHandlers
。
但是在 createReactiveObject
里两个 handlers 的使用逻辑,是基于 target 的 targetType 来判断的,代码如下:
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
)
然后你就发现我们应该需要去看的是 baseHandlers
对应的 mutableHandlers
export const mutableHandlers: ProxyHandler
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 关联 watcher,watcher 的 deps 下面挂着收集起来的 dep。
简单的描述一下全局变量 activeEffect
,它是靠 createReactiveEffect
和 effect
方法(代码在 packages/reactivity/src/effect.ts
里)来进行赋值操作的,比如在 mountComponent 的 setupRenderEffect,就是在 instance 的 update 方法上,挂了一个 effect(function componentEffect() {...}),这里面就会有组件的渲染逻辑。
而我们在触发 getter 的 track 过程中,等于把变量和具有重新渲染视图能力的 activeEffect 绑定在了一起,从而完成了依赖的收集,具体代码如下
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
}
function createReactiveEffect(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect {
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
}
派发更新
派发更新就是 vue 收集到的那些变量在变动时,如何促使视图跟着发生变化
在 2 版本,是靠在变量 set 的时候,dep 的 notify 触发 Watcher 的一系列响应
而上文已经看完了最简单的 get 相关逻辑( get 流程内的分支代码以及 watch 等的实现方式都还没去看),也就是在 reactive 执行时,会把对应的 activeEffect(内置 instance.update) 藏在 get 中等待收集。剩下就是大致看一下 set 触发后,如何调用 到 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
根据依赖收集的结果,现在需要去寻找的核心思路是,怎么在 targetMap 里找到当前 target 对应的 depsMap ,然后找到当前 key 对应的 dep,然后再在 dep(Set 结构)中读取需要执行的函数
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
}
const effects = new Set()
const add = (effectsToAdd: Set | 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)
}
这里主要就是识别一下 trigger 的 type,然后靠 add 收集 effect,最后靠 run 来执行收集起来的 effect,如果中间那一坨看着不太直观,下面是打包出来的 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;
}
扩展阅读(没啥意义)
下面是扩展阅读,没啥太大意义,只是检查除此以为没有其他的依赖收集入口
前文还提到了,使用 reactive 之后,在组件初始化的时候,会走一遍(mountComponent => setupComponent => setupStatefulComponent => handleSetupResult => proxyRefs)这个流程。更直白的讲,我们这里只是在 setup,组件初始化的时候会执行 handleSetupResult 来处理我们 setup 的结果
在 setupStatefulComponent
中创建了一个新的 Proxy,看注释是说一个公用的实例/渲染的 proxy
但是你追进去就会发现,这个只针对 instance.ctx 的 proxy 只有在 dev
模式下,才会把 setup 内 reactive 加工的变量加到 ctx 上 (逻辑在 handleSetupResult
函数内部,exposePropsOnRenderContext
的调用)
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 函数
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)) {
// ... 省略了,你可以全当这里也是在 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
})
})
}
看来并没有遗漏什么重要的逻辑,所以我们就不继续追这个 proxy 了,有兴趣的同学可以继续追一下 PublicInstanceProxyHandlers 的代码,看看有没有什么神奇的地方
扩展阅读结束
下次准备看看什么是 Composition API 的,了解下热点。