vue响应式原理


Vue3 源码解析系列 - 响应式原理(reactive 篇)

reactive

核心入口

话不多说,我们先来看下核心的 reactive 的源码,先看下有哪些依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 工具方法,isObject 是判断是否是对象,toTypeString 获取数据类型
import { isObject, toTypeString } from "@vue/shared";
// Proxy 的 handlers
// mutableHandlers:可变数据的 handler
// readonlyHandlers:只读数据的 handler
import { mutableHandlers, readonlyHandlers } from "./baseHandlers";
import {
mutableCollectionHandlers, // 可变集合数据的 handler
readonlyCollectionHandlers, // 只读集合数据的 handler
} from "./collectionHandlers";
// effect 泛型类型
import { ReactiveEffect } from "./effect";
// ref 泛型类型
import { UnwrapRef, Ref } from "./ref";
// 工具方法,将字符串转化成 Map,返回 function 来判断是否 这个Map 上包含所给的 key
// 这个在 vue2 里也有
import { makeMap } from "@vue/shared";

可以很清楚的看到,重点依赖项就是那一堆 handler 了,其他都是一些工具方法和泛型类型

接下来的源码里是一堆变量的定义,不过我们先跳过,先来看下 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
// 上面还有一大坨变量定义,很关键,但是我们先跳过,先看下有哪些方法,类型是什么样的

// 依赖集合类型
export type Dep = Set<ReactiveEffect>
// 看名字就知道,是 key 和 Dep 集合的对应关系集合
// key 其实就是我们响应式数据上的 key,Dep 则是有哪些地方依赖到了这个 key
// 比如 const a = { foo: 1 },如果在其他两处都用到了 a.foo,那么
// 这里的 key 就是 foo,Dep 就是这两处的 依赖集合
export type KeyToDepMap = Map<string | symbol, Dep>

// 判断对象能不能被观察的
const canObserve = (value: any): boolean

// only unwrap nested ref
// 解套 Ref 类型
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

// 响应式入口方法,入参是泛型,继承 object,返回值就是上面的解套 Ref 类型
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

// 跟 reactive 作用相同,只不过返回值是 Readonly 的
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>>

// 创建响应式对象的关键方法,reactive 和 only 都调用了这个方法
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
): any

// 是否是响应式数据
export function isReactive(value: any): boolean

// 是否是只读的响应式数据
export function isReadonly(value: any): boolean

// 将响应式数据转化为原始数据
export function toRaw<T>(observed: T): T

// 将 value 标记为 Readonly,在 reactive 方法里会判断是否是 Readonly 的原始数据
export function markReadonly<T>(value: T): T

// 将 value 标记为不可响应数据,这个将会影响 canObserve 方法的判断
export function markNonReactive<T>(value: T): T

看完方法和类型,大致有以下几个问题:

  1. Dep 依赖是如何追踪的?
  2. UnwrapRef 是如何展开嵌套的响应式数据类型的(俗称解套),比如 reactive({ name: reactive(ref('Jooger')) })
  3. 如何判断是否是(只读的)响应式数据?往响应式数据上面挂载一个标记位来标记?还是用一个对象来存储响应式数据?
  4. 如何将响应式数据转化为原始数据?proxy 数据如何转化成 object?

问题 1,后面的 effect 会讲,这里先不讨论

问题 2,后面的 ref 会讲,这里先不讨论

问题 3,4 就需要看下我刚才跳过的一堆变量的定义了:

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
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.

// target 和 KeyToDepMap 的映射关系集合
// 一句话理解,有多个 target,每个 target 上有多个 key,每个 key 都有多个依赖
// 至于为什么要把映射关系存到 WeakMap 里,根据上面注释所述,是为了减少内存开销
// 这个在后续的 effect 部分会讲
export const targetMap = new WeakMap<any, KeyToDepMap>()

// WeakMaps that store {raw <-> observed} pairs.
// 下面这四个变量就是为了解答 问题 3 和 4 的
// 根据上面的原英文注释,这四个变量是 raw 和 observed 的对应关系集合
// raw 是原始数据,observed 则是响应式数据

// 原始数据 -> 响应式数据
const rawToReactive = new WeakMap<any, any>()
// 响应式数据 -> 原始数据
const reactiveToRaw = new WeakMap<any, any>()
// 原始数据 -> 只读响应式数据
const rawToReadonly = new WeakMap<any, any>()
// 只读响应式数据 -> 原始数据
const readonlyToRaw = new WeakMap<any, any>()

// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
// 前面提到过的 markReadonly 和 markNonReactive 方法用到的
// 用来存储我们标记的特定数据,以便在创建响应式数据是来检查是否被上面两个方法标记过
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()

// 判断是否是集合类型(Set, Map, WeakMap, WeakSet)
// 因为集合类型的代理 handler 和普通对象是不同的,需要特殊处理
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
// 判断是否是可观察类型,有以下 6 类,在 canObserve 方法里会用到
const isObservableType = /*#__PURE__*/ makeMap(
['Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet']
.map(t => `[object ${t}]`)
.join(',')
)

看完上面这些变量定义,我们来解答一下问题 3,4:

问题 3:如何判断是否是(只读的)响应式数据?往响应式数据上面挂载一个标记位来标记?还是用一个对象来存储响应式数据?

readonlyToRaw 来存储只读响应式数据的,参见下面代码:

1
2
3
export function isReadonly(value: any): boolean {
return readonlyToRaw.has(value);
}

问题 4:如何将响应式数据转化为原始数据?proxy 数据如何转化成 object?

reactiveToRawreadonlyToRaw 来存储响应式数据 -> 原始数据 的映射关系,然后:

1
2
3
export function toRaw<T>(observed: T): T {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed;
}

总的来讲,就是利用各种集合来存储原始数据和响应式数据的映射关系,以便快速根据这种映射关系拿到对应的数据。

再回头看下 canObserve 方法,来看看到底有哪些数据是可以观察的:

1
2
3
4
5
6
7
8
9
10
11
12
const canObserve = (value: any): boolean => {
return (
// Vue 实例不可观察,目前库里还没有 _isVue 的逻辑,不过猜测应该是内部在 setup 方法中挂载
!value._isVue &&
// virtual dom 不可观察
!value._isVNode &&
// 'Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet' 类型以外的不可观察
isObservableType(toTypeString(value)) &&
// 已经标记为不可响应数据的不可观察
!nonReactiveValues.has(value)
);
};

相比于 Vue2 的是否可观察判断,则少了很多条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 我就不解析 Vue2 中的这段判断代码了
// 相比于 Vue2,少了 __ob__ ,ssr 以及 Object.isExtensible 的判断
// 这都是得益于 Proxy
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}

接下来就讲一下重点的 reactivereadonly 这两个核心方法的实现:

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
// 前面讲过返回值就是上面的解套 Ref 类型
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果是一个只读响应式数据,直接返回,因为已经是响应式的了
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
// 如果曾经被标记为只读数据,直接调用 readonly 方法生成只读响应式对象
if (readonlyValues.has(target)) {
return readonly(target)
}

// 创建响应式对象
return createReactiveObject(
target, // 原始数据
rawToReactive, // raw -> observed
reactiveToRaw, // observed -> raw
mutableHandlers, // 可变数据的 proxy handle
mutableCollectionHandlers // 可变集合数据的 proxy handler
)
}

export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>> {
// value is a mutable observable, retrieve its original and return
// a readonly version.
// 如果是响应式数据,那么获取原始数据来进行观察
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
// 创建响应式数据,同样
return createReactiveObject(
target, // 原始数据
rawToReadonly, // raw -> readonly observed
readonlyToRaw, // readonly ovserved -> raw
readonlyHandlers, // 只读数据的 proxy handler
readonlyCollectionHandlers // 只读集合数据的 proxy handler
)
}

其实我们看 Vue3 的源码会发现,很多入口方法都变得短小精简,不像 Vue2 里的一些 exposed function 那样写的很长,这两个核心方法也一样,逻辑很简单,主要是进行一些原始数据检查和转换,核心实现逻辑都是放在 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
// 创建响应式对象
function createReactiveObject(
target: any, // 原始数据
toProxy: WeakMap<any, any>, // raw -> (readonly) observed
toRaw: WeakMap<any, any>, // (readonly) observed -> raw
baseHandlers: ProxyHandler<any>, // 只读/可变 数据的 proxy handler
collectionHandlers: ProxyHandler<any> // 只读/可变 集合数据的 proxy handler
) {
// 如果不是对象,则直接返回自身,包括 null,reactive(null) => null
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// target already has corresponding Proxy
// 如果原始数据已经被观察过,直接通过 raw -> observed 映射,返回响应式数据
let observed = toProxy.get(target);
if (observed !== void 0) {
return observed;
}
// target is already a Proxy
// 如果原始数据本身就是响应式的,直接返回自身
if (toRaw.has(target)) {
return target;
}
// only a whitelist of value types can be observed.
// 如果是不可观察对象,直接返回自身
if (!canObserve(target)) {
return target;
}
// 判断是采用基础数据(object|array)handler 还是集合数据 handler
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers;
// Proxy 创建代理对象,即响应式对象
observed = new Proxy(target, handlers);
// 创建后,设置好 raw <-> observed 的双向映射关系(*)
toProxy.set(target, observed);
toRaw.set(observed, target);
// 上面讲到了 targetMap 的作用,这里是创建默认依赖追踪集合
if (!targetMap.has(target)) {
targetMap.set(target, new Map());
}
return observed;
}

我们可以看到:

  • 目前来看 reactivereadonly 的区别仅有两点:映射关系存储集合不同 and proxy handler 不同
  • object``array 和集合类型 Set``Map``WeakSet``WeakMapproxy handler 是不同的

所以下面再来依次看下响应式核心中的核心 - 各种 proxy handler

baseHandler

方法跟上面一样,先看依赖:

1
2
3
4
5
6
7
8
9
10
11
12
// 上面讲过,不过现在来看感觉像是是在 get set 这些 trap 方法里会调用
import { reactive, readonly, toRaw } from "./reactive";
// 操作类型枚举,对应于 proxy handler 里的 trap 方法
import { OperationTypes } from "./operations";
// 依赖收集和触发依赖回调
import { track, trigger } from "./effect";
// 全局锁,用来禁止 set 和 delete
import { LOCKED } from "./lock";
// 工具方法,类型判断
import { isObject, hasOwn, isSymbol } from "@vue/shared";
// 判断是否是 ref,后面会讲到
import { isRef } from "./ref";

这里有两个疑问:

  1. tracktrigger 的实现
  2. LOCKED 的作用?为什么会有这个全局锁?

问题 1 在后面的 effect 部分会讲到,现在只需要知道是用来追踪依赖和触发依赖回调方法就行

问题 2 现在我也不是特别了解,只知道是在组件 mountupdate 的时候会对组件的 props的代理进行修改,因为我们都知道单向数据流中,子组件内部是不能更改 props 的,但是子组件更新,进行 vnode patch 后需要更新子组件的 props,包括一些动态 props

再来看下变量和方法概览:

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
// JS 内部语言行为描述符集合,比如 Symbol.iterator 这些,在 get 里会用到
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => (Symbol as any)[key])
.filter(isSymbol)
)

function createGetter(isReadonly: boolean) {}

function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean

function deleteProperty(target: any, key: string | symbol): boolean

function has(target: any, key: string | symbol): boolean

function ownKeys(target: any): (string | number | symbol)[]

export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}

export const readonlyHandlers: ProxyHandler<any> = {
get: createGetter(true),

set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
} else {
return set(target, key, value, receiver)
}
},

deleteProperty(target: any, key: string | symbol): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(
key
)}" failed: target is readonly.`,
target
)
}
return true
} else {
return deleteProperty(target, key)
}
},

has,
ownKeys
}

可以看到,mutableHandlersreadonlyHandlers 都是定义了 5 个 trap 方法:getsetdeletePropertyhasownKeys,前 3 个不用多家介绍,has trap 针对与 in 操作符,而 ownKeys 针对于 for inObject.keys 这些遍历操作的

readonlyHandlers 相比于 mutableHandlers 其实只是在 getsetdeleteProperty 这三个 trap 方法里有区别,而对于可能改变数据的 setdeleteProperty 方法,则是利用 LOCKED 来锁定,不让修改数据,这个变量我在上面也提了一下

下面来一个一个的看下各个 trap 方法

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
27
28
29
30
31
// 创建 get trap 方法
// 如果是可变数据, isReadonly 是 false
// 如果是只读数据,那么 isReadonly 就是 true
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
// 利用 Reflect 反射来获取原始值
const res = Reflect.get(target, key, receiver);
// 如果是 JS 内置方法,不进行依赖收集
if (isSymbol(key) && builtInSymbols.has(key)) {
return res;
}
// 如果是 ref 类型数据,则直接返回其 value
// TODO 后面 ref 部分我们会讲到,ref(target) 其实在 get value 的时候做了依赖收集了,
// 就不需要下面重复收集依赖
if (isRef(res)) {
return res.value;
}

// get 类型操作的依赖收集
track(target, OperationTypes.GET, key);

// 这里其实很简单就是递归返回响应式对象
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
};
}

看完 get trap 其实很简单,但是也会有写疑问:

1. 为什么用 Reflect.get,而不是直接 target[key] 返回呢?

我们可以看它的第三个参数:

receiver:如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值

举个例子:

1
2
3
4
5
6
7
8
const target = {
foo: 24,
get bar() {
return this.foo;
},
};

const observed = reactive(target);

此时,如果不用 Reflect.get,而是 target[key],那么 this.foo 中的 this 就指向的是 target,而不是 observed,此时 this.foo 就不能收集到 foo 的依赖了,如果 observed.foo = 20 改变了 foo 的值,那么是无法触发依赖回调的,所以需要利用 Reflect.get 将 getter 里的 this 指向代理对象

2. 为什么在结尾 return 的时候还要调用 reactive 或者 readoonly 呢?

原注释是这样写的:

need to lazy access readonly and reactive here to avoid circular dependency
翻译过来是:需要延迟地使用 readonly 和 readtive 来避免循环引用

为什么这样说呢?这里不得不说一下 Proxy 的特性:只能代理一层,对于嵌套的深层对象,如果不按源码中的方法,那就需要一层层递归来代理劫持对象,即每次递归都判断是否是对象,如果是对象,那么再调用 reactive 来响应式化

但是问题又来了,JS 里是有循环引用这个概念的,就像下面这样:

1
2
3
4
5
const a = {
b: {},
};

a.b.c = a;

这样的话,如果每次递归调用 reactive 的话,会造成调用栈溢出 Maximum call stack size exceeded,但是我们只需要加上一个判断条件即可解决,在上面解析的 createReactiveObject 方法里我们知道如果原始数据已经被观察过,则直接返回对应的响应式数据,那么我们可以在递归调用 reactive 的时候判断 toProxy.get(target) 是否存在,如果存在就不往下递归了,我写了一个例子代码:

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
// 循环引用对象
const target = { b: { c: 1 } };
target.b.d = target;

// 这个就是上面讲的 原始数据 -> 响应式数据的集合
const rawToReactive = new WeakMap();

function reactive(data) {
const observed = new Proxy(data, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
const observed = rawToReactive.get(res);
return observed || res;
},
});
rawToReactive.set(data, observed);

for (let key in data) {
const child = data[key];
// 这里判断如果没有被观察过,那么继续 reactive 递归观察
if (typeof child === "object" && !rawToReactive.get(child)) {
reactive(child);
}
}

return observed;
}

const p = reactive(target);

console.log(p.b.c); // 1

console.log(p.b.d.b); // Proxy {c: 1, d: {…}}

// 我试了一下。跟源码里的 reactive 的 get 结果是一样的

可以去看下我在 vue3 响应式源码解析-Reactive 篇 这篇文章下的评论部分

而源码中的 lazy access 方式很取巧,只代理一层,当用到某个属性值对象时,再进行响应式观察这一层

所以相比于初始化时递归劫持,延迟访问劫持的方式更能提升初始化性能,也有利于对数据劫持做更细的控制,特别是针对于数据对象比较大时(比如接口返回数据嵌套过深),有些数据并非需要劫持,所以按需劫持代理我们用到的数据这种方式更好

set

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
// set trap 方法
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
// 如果是观察过响应式数据,那么获取它映射的原始数据
value = toRaw(value);
// 获取旧值
const oldValue = target[key];
// 如果旧值是 ref 类型数据,而新的值不是 ref,那么直接赋值给 oldValue.value
// 因为 ref 数据在 set value 的时候就已经 trigger 依赖了,所以直接 return 就行
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
// 对象上是否有这个 key,有则是 set,无则是 add
const hadKey = hasOwn(target, key);
// 利用 Reflect 来执行 set 操作
const result = Reflect.set(target, key, value, receiver);
// don't trigger if target is something up in the prototype chain of original
// 如果 target 原型链上的数据,那么就不触发依赖回调
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
// 开发环境操作,只比正式环境多了个 extraInfo 的调试信息
const extraInfo = { oldValue, newValue: value };
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo);
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo);
}
} else {
// 上面讲过,有这个 key 则是 set,无则是 add
if (!hadKey) {
trigger(target, OperationTypes.ADD, key);
} else if (value !== oldValue) {
// 只有当 value 改变的时候才触发
trigger(target, OperationTypes.SET, key);
}
}
}
return result;
}

同样,set trap 看起来也很简单,但是同时也会有一些问题:

1. target === toRaw(receiver) 是什么鬼逻辑?

首先看下

handler.set()developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set#%E5%8F%82%E6%95%B0img

的关于第三个参数的说明:

最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)。
比如,假设有一段代码执行 obj.name = “jen”,obj 不是一个 proxy 且自身不含 name 属性,但它的原型链上有一个 proxy,那么那个 proxy 的 set 拦截函数会被调用,此时 obj 会作为 receiver 参数传进来

上面已经给出例子了,这里我再写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const child = { name: "child" };

const parent = new Proxy(
{ name: "parent" },
{
set(target, key, value, receiver) {
console.log(target, receiver);
return Reflect.set(target, key, value, receiver);
},
}
);

Object.setPrototypeOf(child, parent);

child.foo = 1;

// 会打印出
// {name: "parent"} {name: "child"}

这里有两个先决条件:

  1. child 的原型链是一个 Proxy
  2. child 在设置值的时候,本身不包含 key 的

可以看到,当满足上面两个条件的时候,设置 child 的值,会触发原型链上的 set trap 方法,并且 target 是原型链数据,而 receiver 则是真实数据

所以,源码中的那个条件逻辑也就不难看懂了,当满足上述两个条件时,我们当然不希望触发 parent 的 set trap

2. 像数组的 unshiftsplice 这些操作是如何触发 set trap方法的呢?

1
2
3
4
5
6
7
8
// 在 set 里加上这么一个 log
console.log(
!hadKey ? "add" : value !== oldValue ? "set" : "unknow",
target,
key,
value,
oldValue
);

然后

1
2
3
4
5
6
7
8
9
a = reactive([1, 2, 3]);
a.unshift(0);

// 会打印
// add [1, 2, 3, 3] 3 3 undefined
// set [1, 2, 2, 3] 2 2 3
// set [1, 1, 2, 3] 1 1 2
// set [0, 1, 2, 3] 0 0 1
// unknow [0, 1, 2, 3] length 4 4

一共打印了 5 次,根据打印内容我们可以看到 unshift 的实际操作过程,即把数组的每一项依次都往后移动一位,然后再把首位设置成 0,至于为什么这么操作,

ECMA-262 Array.property.unshiftwww.ecma-international.org/ecma-262/6.0/#sec-array.prototype.unshift

标准中有原理介绍,我就不赘述了,还有像 shiftsplice 也是一样的操作步骤

可以看到 unshift 或者 splice 是会带来多次的 trigger 的,当然这些会有批量跟新优化的,有时间我再展开讲一下

细心的同学可能会发现,还触发了 length 属性的 set,而且 valueoldValue 是一样的,那么根据源码所示,就不会触发 set 类型的回调了呀,那我们如果在 template 里用到了 a.length 那也不会更新了么?

肯定是会更新的,解决办法就在 trigger 这个方法里,后续 effect 部分会讲到,先简单说一下,对于会导致数组 length 改变的操作,比如 add 和 delete,在 effecttrigger 方法里会单独处理,来触发 length 属性的依赖回调的

其他 trap 方法

还有 deletePropertyhasownKeys 这几个 trap,代码不多,都很简单,直接看下面的源码就能明白,我就不在赘述了

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
// deleteProperty trap 方法
function deleteProperty(target: any, key: string | symbol): boolean {
const hadKey = hasOwn(target, key);
const oldValue = target[key];
const result = Reflect.deleteProperty(target, key);
if (result && hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue });
} else {
trigger(target, OperationTypes.DELETE, key);
}
}
return result;
}

// has trap 方法
function has(target: any, key: string | symbol): boolean {
const result = Reflect.has(target, key);
track(target, OperationTypes.HAS, key);
return result;
}

// ownKey trap 方法
function ownKeys(target: any): (string | number | symbol)[] {
track(target, OperationTypes.ITERATE);
return Reflect.ownKeys(target);
}

总结

  • baseHandler 是针对于数组和对象类型的数据的 proxy handler
  • 每个 trap 方法都是用 Reflect 来反射到原始数据上的
  • 对于 gethasownKeys 这一类读操作,会进行 track 来收集依赖,而对于 setdeleteProperty 这类写操作,则是会进行 trigger 来触发依赖回调
  • 响应式数据的读取是 lazy 的,即初始化的时候不会对嵌套对象全盘观察,而是只有用到了每个值才会生成对应的响应式数据

collectionHandler

还记得我们在看 reactive 方法那里有个 collectionTypes 的判断对吧,collectionHandler 就是专门来处理 Set|Map|WeakSet|WeakMap 这类集合类型数据的

这里可以参考相学长的

vue3 响应式源码解析-Reactive 篇 - 掘金 juejin.im/post/5da9d7ebf265da5bbb1e52b7#heading-12

这篇文章,写的很详细,我这里也不再赘述了

总结

最开始阅读 reactive 源码时,总体的逻辑是比较清晰的,但是仍然有几个地方当时有疑惑:

  • rawToReactive|rawToReadonly 等这几个变量是干嘛的?
  • targetMap 的是干什么的?为什么是 WeakMap<any, KeyToDepMap> 类型
  • LOCKED 是用来干嘛的?
  • baseHandlerget trap 为什么又返回了一个 reactive(res)
  • collectionHandler 里的 trap 方法为什么只有 get?为什么跟 baseHandler 不一样?

在读完源码后,除了 LOCKED 那个疑惑,其他几个问题我都已经找到答案,并且也在上面解惑了,我相信大家看完这篇文章后也应该都有自己的答案了

最后再来个源码里的知识点总结吧:

  • reactive 是利用 Proxy 来进行数据观察,Reflect 相关操作来反射到原始数据的,并且数据的访问是一个 lazy reactive 方式,即按需观察
  • 普通对象、数组和集合类型数据的代理 handler 是不同的,这是因为 Proxy 的一些限制,参考

Proxy and Reflectjavascript.info/proxy#proxy-limitationsimg

  • 利用几个 WeakMap 来存储原始数据 <-> 响应式数据的双向映射关系,以便在响应式入口方法里判断是否原始数据已经被观察过,这个相比于 Vue2 的直接在原始数据上挂载 __ob__要少一些冗余数据,并且由于 WeakMap 的 GC 特性,在运行时会有一定的内存优化
  • 响应式数据的读操作会 track 来收集依赖,写操作则是会 trigger 来触发依赖回调

整个 reactive|readonly 的流程如下:

img


文章作者: olddog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 olddog !
  目录