Vue3.0Reactivity之reactive

前言

开始来写一些关于Vue3的reactivity包源码的理解了,可能某些地方会有错误,希望可以指出,感激不尽~

因为这个包中其实是没有watchEffectwatch这两个API的,这两个API在别的包中,所以可能在后面再讲,因为我还没看…

对于这个包,主要的API为reactivereadonlyrefcomputed以及没有在全局Vue对象暴露的effect

reactive

首先,需要去单独的把这个包编译成一个在浏览器端可以引入的文件,这里可以看这个包下面的README.md

可以结合之前的帖子 Vue3.0尝鲜这个帖子。

直接在vue-next下直接运行yarn build reactivity --types就可以编译出文件,选择reactivity.global.js即可。

这次讲下reactive方法。在前面关于Reactivity主要API的翻译可以知道,reactive这个函数主要是包装一个对象,返回一个响应式的对象。

在Reactivity的包中的src下,可以找到reactive的ts文件,其中对reactive的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果尝试去观察一个只读的代理,直接返回只读的版本即可
if (readonlyToRaw.has(target)) {
return target
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}

这里可以看到,主要的创建逻辑是createReactiveObject这个方法,但是这个readonlyToRaw是什么东西呢

可以在前面的定义中,发现有四个WeakMap的定义

1
2
3
4
5
6
// WeakMaps that store {raw <-> observed} pairs.
// 存储原生对象(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>()

根据官方的注释和变量的名字不难看出,这四个WeakMap用来存储原生对象和观察者对象之间的_双向关系_

其中

  • rawToReactive: 原生对象 -> 响应式对象
  • reactiveToRaw: 响应式对象 -> 原生对象
  • rawToReadonly: 原生对象 -> 只读对象
  • readonlyToRaw: 只读对象 -> 原生对象

可以猜测,每创建一个代理,就会通过这些WeakMap建立代理与源对象的双向关系。

ok,我们继续看createReactiveObject这个方法,这个方法传入了五个参数,分别是target(源对象),两个关于响应式对象的WeakMap,以及两个handlers,

这两个handlers又是什么东西呢,可以看到头部引用了另一个文件的好几个不同的handlers

1
2
3
4
5
6
7
8
9
10
import {
mutableHandlers,
readonlyHandlers,
shallowReactiveHandlers,
shallowReadonlyHandlers
} from './baseHandlers'
import {
mutableCollectionHandlers,
readonlyCollectionHandlers
} from './collectionHandlers'

从文件名来看,baseHandlers应该是基础的一种handlers,而collectionHandlers应该是关于集合的一种handlers,

然后从引入的handlers名字来看,

  • mutableHandlers:可变handlers
  • readonlyHandlers:只读handlers
  • shallowReactiveHandlers:浅响应handlers
  • shallowReadonlyHandlers:浅只读handlers
  • mutableCollectionHandlers:可变集合handlers
  • readonlyCollectionHandlers:只读集合handlers

这里我们可以看到mutableHandlers的定义

1
2
3
4
5
6
7
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}

是不是很熟悉,就是Proxy第二个参数中可以配置的拦截函数。

对于这个handler,先不讲那么深,只要知道,这个handlers就拦截了操作

先看之前说的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
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target already has corresponding Proxy
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
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}

首先是第一段

1
2
3
4
5
6
7
8
9
10
11
function createReactiveObject(
// ...
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// ...
}

很明显,根据函数名就可以判断,如果不是对象的话,直接返回源对象,也就是不能对基本类型进行响应式包装。

__DEV__是一个开发环境的变量,如果在开发环境下,就会打印一则警告来提醒用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createReactiveObject(
// ...
) {
// ...
// target already has corresponding Proxy
// 目标早已有相应的代理
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// target is already a Proxy
// 目标本身就是一个代理
if (toRaw.has(target)) {
return target
}
// ...
}

如果源对象存在代理对象或者源对象已是一个代理时,就直接返回,防止创建多个响应式对象。

1
2
3
4
5
6
7
8
9
10
11
function createReactiveObject(
// ...
) {
// ...
// only a whitelist of value types can be observed.
// 只要在白名单中的类型才可以被观察
if (!canObserve(target)) {
return target
}
// ...
}

这句不难理解,判断对象是否可以被观察,这个函数的定义为

1
2
3
4
5
6
7
8
9
const canObserve = (value: any): boolean => {
return (
!value._isVue &&
!value._isVNode &&
isObservableType(toRawType(value)) &&
!rawValues.has(value) &&
!Object.isFrozen(value)
)
}

根据变量名可以读出,这个对象必须

  • 不是Vue对象
  • 不是VNode对象
  • 在可响应式的类型中
  • 没被冻结
  • 原生对象Map不含这个对象

转到isObservableType的实现,可以确定可观察的类型只有
Object,Array,Map,Set,WeakMap,WeakSet这几种类型

1
2
3
const isObservableType = /*#__PURE__*/ makeMap(
'Object,Array,Map,Set,WeakMap,WeakSet'
)

最后一段,便是创建代理对象了

1
2
3
4
5
6
7
8
9
10
11
12
13
function createReactiveObject(
// ...
) {
// ...
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
// ...
}

首先是判断源对象的类型,用构造器去判断,然后使用对应的handlers创建Proxy代理对象。然后保存代理对象和源对象的关系(通过前文说到的WeakMap),最后返回这个响应式对象。

至此,对于响应式对象创建的基本流程已经明朗

回过头来,为什么返回的这个代理对象就是响应式的呢?

这需要我们去看创建响应式的handlers的内容了

我们在前面有说到,这个文件引入了多个handlers,其中一个为

1
2
3
4
5
6
7
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}

接下来,我们就从这里入手,探究何为响应式。

我们可以去看Vue的RFC文档

Composition API RFC | Vue Composition API

里面关于Detailed Design(详细设计)中的API Introduction(API介绍)有一段我觉得很好的解释了关于响应式这个含义

1
2
3
4
5
6
7
8
9
import { reactive, watchEffect } from 'vue'

const state = reactive({
count: 0
})

watchEffect(() => {
document.body.innerHTML = `count is ${state.count}`
})

这一段话中我觉得下面这句话(以及简短的代码段)间接明了地解释了响应式

Thanks to dependency tracking, the view automatically updates when reactive state changes.

这句话大意是:多亏了依赖的收集,视图可以自动地在响应式状态的值发生改变时而更新。

对应到代码中,就是当state.count发生改变时,使用到state.count的函数便会自动的重新执行。

那么现在其实问题就很明了了,如何收集依赖?也就是收集变量改变的函数?
如何触发依赖?也就是在变量改变时重新执行跟它有关的函数?

这里我用effect代替watchEffectwatchEffect就是在effect上扩展的,包中是没有watchEffect的),测试的动图如下:

ok,我们开始来看mutableHandlers中的get属性,发现它的定义为

1
const get = /*#__PURE__*/ createGetter()

我们接着看createGetter这个函数

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
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)

if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}

if (shallow) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
}

if (isRef(res)) {
if (targetIsArray) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays.
return res.value
}
}

!isReadonly && track(target, TrackOpTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}

哇,这么长,我看不懂啊啊啊啊

其实,现在并不需要全部都看,我们可以发现函数中除了一个track函数

也就是所谓的收集依赖函数,我们可以点进去看,发现这个函数定义在effect.ts文件中

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

这里出现几个全局定义的变量,分别是shouldTrackactiveEffecttargetMap

我们可以往前找,找到这三个的定义

1
2
3
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
1
2
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
1
2
let shouldTrack = true
const trackStack: boolean[] = []

根据变量的语义不难推出意思,

其中targetMap存储:对象 -> 属性,而这个属性也是一个Map,存储 属性 -> ReactiveEffect,这个ReactiveEffect是一个Set。

activeEffect表示当前的活动Effect。

shouldTrack表示应不应该收集依赖。

ok,接着我们分析这个函数,

1
2
3
4
5
6
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
// ...
}

开头先判断如果不应该收集,或者当前活动的Effect为undefined,直接返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function track(target: object, type: TrackOpTypes, key: unknown) {
// ...
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()))
}
// ...
}

这一段,找出对象的依赖Map,也就是 Map&lt;属性名,Set&lt;Effect&gt;&gt;

再找出对应属性名的Set,也就是Set&lt;Effect&gt;

并且对这两个做了空判断并初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function track(target: object, type: TrackOpTypes, key: unknown) {
// ...
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}

最后,判断如果当前的effect不在Set&lt;Effect&gt;中,就把这个effect存进去,并且在当前effect上的deps上把拥有自己的Set也给存进去。然后就是在开发模式下调用用户传入的onTrack函数了。

这里可能会疑问,如何知道当前的effect就是这个target的key的依赖呢?

这里需要把目光先转向effect函数

在前面的一个浏览器跑的例子,我们知道

effect传入一个函数,函数内部使用响应式变量就能收集这个函数作为依赖。

在同一个文件内可以找到

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

发现effect的实现不难,主要的逻辑在createReactiveEffect函数中。找到它

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
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args)
} 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
}

对于这么长的代码,只需关注最重要的一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
// ...
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
// ...
}

首先判断自己是不是在effect的栈中,如果在栈中,要对这个effect进行清理初始化,

然后enableTracking(),保存上一个shouldTrack的状态,并把当前shouldTrack置为true,然后把自己保存到effect栈的栈顶,并将activeEffect指向自身。

接着,直接运行这个传进来的函数

注意,还记得吗,这里运行就会触发响应式对象get拦截方法中的track方法,而track方法会将当前的effect与对应对象的属性名进行关联,

最后在finally块中将当前的effect弹出,重置shouldTrack为上一次的状态的,把当前的effect指向栈的倒数第二个effect。恢复了上一次的状态。

为什么要保存历史的shouldTrack状态,因为在一个函数中,可能会触发多个响应式变量属性的get

所以,对于前面的如何知道当前的effect就是这个target的key的依赖呢?这个问题,到这里基本就解决了。

接着,我们需要理明白如何在改变响应式对象的值之后,触发依赖(也就是执行对应的函数)

我们回到mutableHandlers,这次我们看下set的实现

1
const set = /*#__PURE__*/ createSetter()

发现他也是运行一个函数,我们转到这个函数

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
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 = 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的函数,根据英文知道它的意思是触发的意思。可以判断它应该就是触发依赖的函数。我们转到这个函数

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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 computedRunners = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}

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 isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && 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()
}
}

// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}

这么长的函数,只需关注重要的地方即可。

我们发现开头就是把对象的属性Map给取了出来。

然后我们只需注意add函数和run函数,add函数把传进来的依赖添加到effectscomputedRunners(这个是计算属性,也是由effect实现,之后会将),

然后中间是一大堆的if判断,我们只需看其中的一句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function trigger(
// ...
) {
// ...
if (/* ... */) {
// ...
} else if (/* ... */) {
// ...
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// ...
}
}

我们发现,这里把获取了传进来属性名(key)的Set<Effect>,并且添加在上面说到的两个数组中。

然后定义了一个run函数,这个函数开头处理了开发模式下的onTrigger函数,最后,执行了传进来的effect

最后遍历两个前面说到的数组,执行了全部和targetkey有关的effect。

至此,响应式的原理基本上已经明了了。

我们可以用一段很简单的代码来梳理整个流程

1
2
3
4
5
6
7
8
9
const {reactive, effect} = VueReactivity;

const state = reactive({
count: 0
});

effect(() => {
console.log(state.count);
});

首先我们通过reactive函数创建一个响应式对象,这个响应式对象代理了源对象,它的setget中分别有track(收集依赖)和trigger(触发依赖)这两个操作。

然后我们通过effect来执行我们的函数,在执行中,会标记这个函数,也就是activeEffect,然后运行它,

一运行,如果使用了响应式的变量,就会触发代理对象的get,执行了track函数

就把当前运行的函数和这个响应式对象建立一个双向的连接。

当我们修改响应式变量,比如执行state.count++时,会触发代理对象的set,执行了trigger函数,

trigger函数会通过之前建立的连接,找到和自己相关的effect,然后执行。

我感觉如果明白了基本的流程,那源代码看起来应该不难,因为知道下一步要干什么,剩下的就是一些判断之类的代码了。

后记

第一次写这么长的笔记,如果哪里写的不好希望提出来,非常感谢!