从需求出发阅读 Vue 源码之响应式基础(Vue 3)
在 JavaScript 中有两种劫持 property 访问的方式:getter / setter 和 Proxy。Vue 2 使用 getter / setter 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。
从数据变动时自动更新页面渲染的需求出发,我们在从需求出发阅读 Vue 源码之响应式基础(Vue 2)这篇笔记中,了解到 Vue 2 使用了 getter / setter 劫持 property 访问,现在还是针对同样的需求看一下 Vue 3 是如何做的。
先来把测试程序修改一下,让程序在挂载后延迟 2 秒更改 message
的值:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue Reactivity</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<div>{{ message }}</div>
</div>
</body>
<script>
const { createApp } = Vue
const app = createApp({
data: () => ({
message: 'hello vue!',
}),
mounted() {
setTimeout(() => {
this.message += ' hello world!'
}, 2000)
},
})
app.mount('#app')
</script>
</html>
打开 packages/runtime-core/src/componentOptions.ts,我们曾在从需求出发阅读 Vue 源码之声明式渲染(Vue 3)中提到过,第 610 行 applyOptions
函数会在 728 行执行 reactive
函数以此来实现响应式。
import {
reactive,
......
} from '@vue/reactivity'
export function applyOptions(instance: ComponentInternalInstance) {
......
instance.data = reactive(data)
......
}
打开 packages/reactivity/src/reactive.ts,第 83 行定义并导出了 reactive
函数,在函数中执行了 createReactiveObject
并传入了目标对象和处理器对象等参数。
......
import {
mutableHandlers,
......
} from './baseHandlers'
import {
mutableCollectionHandlers,
......
} from './collectionHandlers'
......
export const reactiveMap = new WeakMap<Target, any>()
......
export function reactive(target: object) {
......
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
在 createReactiveObject
函数中创建 Proxy 对象时对目标对象的类型作了判断,对于 Object、Array 使用 baseHandlers
处理器,对于 Map、Set、WeakMap、WeakSet 则使用 collectionHandlers
处理器。Proxy 对象创建成功后,会存储在 reactiveMap
中,键名是目标对象。
import { ...... toRawType ...... } from '@vue/shared'
// export const objectToString = Object.prototype.toString
// export const toTypeString = (value: unknown): string =>
// objectToString.call(value)
// export const toRawType = (value: unknown): string => {
// // extract "RawType" from strings like "[object RawType]"
// return toTypeString(value).slice(8, -1)
// }
......
export const enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
......
const enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
......
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
......
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific 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
}
......
接下来打开 packages/reactivity/src/baseHandlers.ts 来看一下处理器对象的实现,第 88 行定义了基础响应式处理器类,在基础响应式处理器类中定义了取值函数,在取 data
对象属性值时,会用 track
函数进行跟踪依赖,并判断值是否为是对象,是则继续递归地设置响应性,最后返回取到的对象属性值。
......
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly _shallow = false
) {}
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
shallow = this._shallow
......
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
......
if (isObject(res)) {
// 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
}
}
打开 packages/reactivity/src/effect.ts,第 247 行定义并导出了 track
函数,第 255 行执行了 createDep
函数,这里能看出一个属性 key
对应一个 dep
依赖集合,和 Vue 2 有相似之处。第 262 行执行了 trackEffects
函数跟踪副作用。
......
const targetMap = new WeakMap<object, KeyToDepMap>()
......
export let activeEffect: ReactiveEffect | undefined
......
export let shouldTrack = true
......
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
......
trackEffects(dep, ......)
}
}
......
打开 packages/reactivity/src/dep.ts,第 21 行定义并导出了 createDep
函数。
......
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0
dep.n = 0
return dep
}
......
回到 packages/reactivity/src/effect.ts,第 266 行定义并导出了 trackEffects
函数,函数执行时会在 dep
集合中加入 activeEffect
当前副作用,并在 activeEffect
的 deps
数组中添加 dep
,这一步和 Vue 2 的依赖收集也有相似之处。
......
export function trackEffects(
dep: Dep,
......
) {
......
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
......
}
}
......
那当前副作用 activeEffect
是什么时候产生的呢?这个在从需求出发阅读 Vue 源码之声明式渲染(Vue 3)已有提到,打开 packages/runtime-core/src/renderer.ts,在执行第 1182 行的 mountComponent
函数时在第 1242 行执行了 setupRenderEffect
函数,又在setupRenderEffect
函数中第 1545 行创建了 ReactiveEffect
类实例(一个应用(app)对应了一个ReactiveEffect
类实例,和 Vue 2 也有相似之处)。随后,在执行 effect
的 run
方法时将当前实例对象赋值给了 activeEffect
。
......
const effect = (instance.effect = new ReactiveEffect(
......
))
const update: SchedulerJob = (instance.update = () => effect.run())
......
update()
......
packages/reactivity/src/effect.ts 中第 100 行将当前 ReactiveEffect
类实例赋值给了当前副作用 activeEffect
。
......
export let activeEffect: ReactiveEffect | undefined
......
export class ReactiveEffect<T = any> {
......
run() {
......
activeEffect = this
......
}
......
回到 packages/reactivity/src/baseHandlers.ts,第 165 行定义了 MutableReactiveHandler
类,这个类继承了 BaseReactiveHandler
类,扩展了 set
等方法,在 set
方法中除了设置值之外,在对应值发生变化时会执行 trigger
函数触发副作用。
......
class MutableReactiveHandler extends BaseReactiveHandler {
constructor(shallow = false) {
super(false, shallow)
}
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
......
const result = Reflect.set(target, key, value, receiver)
...... if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
......
return result
}
......
}
......
打开 packages/reactivity/src/effect.ts,第 305 行定义并导出了函数 trigger
,在函数中获取上文 depsMap
中已添加的依赖项,并使用 triggerEffects
触发副作用。
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
}
let deps: (Dep | undefined)[] = []
......
if (key !== void 0) {
deps.push(depsMap.get(key))
}
......
if (deps.length === 1) {
if (deps[0]) {
......
triggerEffects(deps[0])
......
}
}
}
第 393 行定义并导出了 triggerEffects
函数,并在其中继续调用 triggerEffect
触发副作用。
export function triggerEffects(
dep: Dep | ReactiveEffect[],
......
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
......
triggerEffect(effect, ......)
......
}
......
}
在第 411 行定义了函数 triggerEffect
,最终其会执行上文提到的副作用 effect
的 run
方法去更新页面渲染,至于在更新过程中的异步更新队列、diff 算法、nextTick 等我们在之后的笔记中再去记录。
function triggerEffect(
effect: ReactiveEffect,
......
) {
if (effect !== activeEffect || effect.allowRecurse) {
......
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
在这一 track 跟踪和 trigger 触发的整个过程也应用到了发布订阅设计模式。
接下来,在 packages/reactivity/src/baseHandlers.ts 中使用 MutableReactiveHandler
类创建实例并赋值给 mutableHandlers
导出,到这里处理器对象就创建成功了。
......
export const mutableHandlers: ProxyHandler<object> =
/*#__PURE__*/ new MutableReactiveHandler()
......
打开 packages/runtime-core/src/componentOptions.ts,第 610 是 applyOptions
函数,第 620 行和第 801 行分别判断是否传入了 beforeCreate
函数和 created
函数,是的话则执行,而 mounted
函数则需通过 registerLifecycleHook
注册。
......
import {
......
onMounted,
......
} from './apiLifecycle'
......
export function applyOptions(instance: ComponentInternalInstance) {
......
function registerLifecycleHook(
register: Function,
hook?: Function | Function[]
) {
if (isArray(hook)) {
hook.forEach(_hook => register(_hook.bind(publicThis)))
} else if (hook) {
register(hook.bind(publicThis))
}
}
......
registerLifecycleHook(onMounted, mounted)
......
}
......
打开 packages/runtime-core/src/apiLifecycle.ts,第 74 行通过调用 createHook 方法返回一个函数赋值给 onMounted
,执行 onMounted
时实则是将修改实例 instance
属性 m
值,如果 instance.m
不存在则置为空数组,然后将经过包裹的 wrappedHook
方法添加到 instance.m
数组中等待执行,包裹的目的为暂停依赖收集、设置当前实例和错误处理。
......
export function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
if (target) {
const hooks = target[type] || (target[type] = [])
// cache the error handling wrapper for injected hooks so the same hook
// can be properly deduped by the scheduler. "__weh" stands for "with error
// handling".
const wrappedHook =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
if (target.isUnmounted) {
return
}
// disable tracking inside all lifecycle hooks
// since they can potentially be called inside effects.
pauseTracking()
// Set currentInstance during hook invocation.
// This assumes the hook does not synchronously trigger other hooks, which
// can only be false when the user does something really funky.
setCurrentInstance(target)
const res = callWithAsyncErrorHandling(hook, target, type, args)
unsetCurrentInstance()
resetTracking()
return res
})
if (prepend) {
hooks.unshift(wrappedHook)
} else {
hooks.push(wrappedHook)
}
return wrappedHook
}
......
}
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) => ......
injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)
......
export const onMounted = createHook(LifecycleHooks.MOUNTED)
// export const enum LifecycleHooks {
// ......
// MOUNTED = 'm',
// ......
// }
......
对于 beforeMount
也是类似处理,这样在 packages/runtime-core/src/renderer.ts 第 1310 行判断 bm
是否存在,是则调用执行,第 1390 行判断 m
是否存在,是则放到异步队列里调用执行。
......
const setupRenderEffect: SetupRenderEffectFn = (
instance,
......
) => {
const componentUpdateFn = () => {
......
const { bm, m, parent } = instance
......
if (bm) {
invokeArrayFns(bm)
}
......
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
......
}
}
......
至此,当我们让测试程序在挂载后延迟 2 秒更改 message
的值时,页面也会同步反映出变化。
现在让我们改一下测试程序,延时 2 秒后为 car
对象的 color
属性赋值:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue Reactivity</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<!-- <div>{{ message }}</div> -->
<div>{{ car.color }}</div>
</div>
</body>
<script>
const { createApp } = Vue
const app = createApp({
data: () => ({
// message: 'hello vue!',
car: {},
}),
mounted() {
setTimeout(() => {
// this.message += ' hello world!'
this.car.color = 'red'
}, 2000)
},
})
app.mount('#app')
</script>
</html>
这在 Vue 2 中是观察不到页面变化的,但是由于 Vue 3 使用了 Proxy 拦截访问,所以当 data 选项中设置了对象值,而对象属性没有初始化时,修改属性值也是响应性的,在 IE11 中 Proxy 这个特性无法用 polyfill 来兼容,因此 Vue 3 是不能在 IE 11 中使用的。
讽刺的是,通过在 Vue 3 中支持 IE11,我们给了它更多的生命力。考虑到我们的用户基础,放弃对 IE11 的支持可能会让它更快地被淘汰。
微软官方如今也停用 IE 11,推荐用户安装 Edge。
接下来再次修改测试程序,延时 2 秒后根据数组下标去修改数组,同样也是可以直接反映到页面的,而不用像 Vue 2 一样使用特定的数组方法或者替换整个数组去触发更新。
值得注意的是,在 Vue 2 中没有对 Map、Set、WeakMap、WeakSet 这些数据结构做响应式处理,如果想要修改他们并更新页面必须做替换。还记得上文判断过目标对象类型然后选择不同处理器对象吗?在 Vue 3 中对这些数据结构的 get 、set 方法也做了处理,具体可以参考 packages/reactivity/src/collectionHandlers.ts,第 370 行返回了 mutableCollectionHandlers
对象,实现思路和 baseHandlers
是类似的,有兴趣的小伙伴可以自行阅读。
至此,Vue 3 响应式基础完成。