vue 响应

主要实现逻辑的代码在文件夹packages/reactivity
代码文件包含如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── baseHandlers.ts
├── collectionHandlers.ts
├── computed.ts
├── deferredComputed.ts
├── dep.ts
├── effect.ts
├── effectScope.ts
├── index.ts // 导出所有API的文件
├── operations.ts
├── reactive.ts
├── ref.ts
└── warning.ts

整体流程图:

整体流程图
图片来源地址:Vue3 One Piece

这篇将看一下reactive.ts内部实现了哪些方法

reactive

Vue3中响应数据核心是 reactive , reactive 中的实现是由 proxy 加 effect 组合,先来看一下 reactive 方法的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

/**
* Creates a reactive copy of the original object.
* 创建原始对象的反应副本。
* The reactive conversion is "deep"—it affects all nested properties. In the
* ES2015 Proxy based implementation, the returned proxy is **not** equal to the
* original object. It is recommended to work exclusively with the reactive
* proxy and avoid relying on the original object.
* 反应式转换是“深度”的——它影响所有嵌套的属性。 在基于 ES2015 Proxy 的实现中,返回的代理不等于原* 始对象。 建议专门使用反应式代理并避免依赖原始对象。
* A reactive object also automatically unwraps refs contained in it, so you
* don't need to use `.value` when accessing and mutating their value:
* 反应对象也会自动解包其中包含的引用,所以你
* 在访问和改变它们的值时不需要使用 .value :
* ```js
* const count = ref(0)
* const obj = reactive({
* count
* })
*
* obj.count++
* obj.count // -> 1
* count.value // -> 1
* ```
*/
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果尝试观察只读代理,则返回只读版本。
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false, // isReadonly
mutableHandlers,
mutableCollectionHandlers,
reactiveMap // 记录已经收集的代理对象
)
}

export function isReadonly(value: unknown): boolean {
// 判断有没有已经添加的标记来实现
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

export const enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}

从代码来看,先判断是否已经代理并标记了只读,如果是直接返回当前。如果没有调用函数createReactiveObject实现对象代理,并返回

createReactiveObject 函数

都在代码里了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, 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
// target 已经是 Proxy,返回它。
// 异常:在反应对象上调用 readonly()
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
// 如果这里我手动改变原始对象中的数据,是否可以继续获取到?
// 可以取到WeakMap 引用对象的时候应该是已内存地址作为key
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
}
// 调用proxy生成代理
const proxy = new Proxy(
target,
// 根据数据类型返回不同的处理函数
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 收集已经创造的代理
// 这里收集一下,是为了防止创造了两份代理,导致响应时重复执行两次?
proxyMap.set(target, proxy)
return proxy
}

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
}
}

function getTargetType(value: Target) {
// Object.isExtensible 判断对象是否可以扩展
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}

阅读代码后,可以确定在排除各种边界情况后。所有的后续操作都指向了代理的钩子处理函数上。同时不同的数据类型具有不同的钩子函数

ObjectArray使用的钩子函数是baseHandlers

Map, Set, WeakMapWeakSet, 使用的钩子函数是collectionHandlers

但是这两个个只是形参的名字,实际入参需要看调用的位置具体的后续钩子函数的定义会在后面继续深入谈。
这里只查看reactive的文件实现的逻辑

readonly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Creates a readonly copy of the original object. Note the returned copy is not
* made reactive, but `readonly` can be called on an already reactive object.
*/
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true, // isReadonly
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}

从代码来看与reactive方法一样,都是调用的createReactiveObject函数,只是入参不一致
但是从代码来看,判断是否是只读是通过 target[ReactiveFlags.RAW]是否包含这个属性来决定的,
但是在createReactiveObject函数只是判断了是否是只读,是的话直接返回。并没有看到添加这个标记的位置。这里直接剧透

它是在readonlyHandlersget钩子中加入的详细代码如下()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 这里返回是否是reactive,readonly,shallow的标记响应
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
}
// ...后面还有很多逻辑
}
}

shallowReactive

和 reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

1
2
3
4
5
6
7
8
9
10
11
12
13

export function shallowReactive<T extends object>(
target: T
): ShallowReactive<T> {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
)
}

shallowReadonly

和 readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

1
2
3
4
5
6
7
8
9
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}

toRaw

根据一个 Vue 创建的代理返回其原始对象。

1
2
3
4
5
6

export function toRaw<T>(observed: T): T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}

从代码来看,只是取值,然后判断是否是在绝对是否是递归调用还是直接返回值。

但是有取值,那么真正逻辑就在handers中的get了,下面是相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// ... 其他代码
else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
// 省略很多代码
}
}

从代码来看就是匹配到ReactiveFlags.RAW这个key以后,根据target去不同的WeakMap中获取有没有创建的代理,在和当前的代理进行判断,是不是同一个代理,如果是的话直接返回target

这里有一个疑问,为什么不直接返回代理记录的target呢?要多这么一步

markRaw

将一个对象标记为不可被转为代理。返回该对象本身。

官网文档

1
2
3
4
5
6
7
8
9
10
11
12
export function markRaw<T extends object>(value: T): Raw<T> {
def(value, ReactiveFlags.SKIP, true)
return value
}

export const def = (obj: object, key: string | symbol, value: any) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}

这里就是在当前对象上加入了特殊的属性,后续数据处理时跟着特殊属性特殊处理

is开头的函数和to开头的函数

好了到这里为止,基本已经看完了reactive.ts中主要的函数以及代码实现,还有一些is开头进行判断对象属性的函数没有详细查看下面仅列出,基本都是返回对应的标记枚举,真正判断与返回逻辑都在get钩子函数中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

export function isReactive(value: unknown): boolean {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.RAW])
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

export function isReadonly(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

export function isShallow(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
}

export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
}

export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value // 判断是否是对象是的话直接调用reactive,不是的话直接返回原始值

export const toReadonly = <T extends unknown>(value: T): T =>
isObject(value) ? readonly(value as Record<any, any>) : value // 判断是否是对象是的话直接调用readonly,不是的话直接返回原始值

结尾

从目前的代码来看我们可以得出几个结论

  • reactive只支持引用类型的数据
  • 该文件中主要是各种API的封装具体的逻辑分散在各个API对应的handlers
  • 对于不同的数据类型,大体上分为两种handlers,即baseHandlers.tscollectionHandlers.ts

下面继续按照源代码文件探索 baseHandlers.ts