深入响应式系统

宇宙免责声明:本文不是知识梳理,旨在抛砖引玉!!!

前置

Proxy

想象你是一个高级小区的业主,你的公寓门口配备了诸多门卫,以监控和干预所有进出公寓的行为

Intro

一个 Proxy 对象(门卫)包装另一个对象并拦截诸如读取/写入属性和其他操作,可以选择自行处理它们,或者透明地允许该对象处理它们。

1
let proxy = new Proxy(target, handler)
  • target —— 是要包装的对象(公寓),可以是任何东西,包括函数。
  • handler —— 带有捕捉器(即拦截操作的方法)的对象。比如 get 捕捉器用于读取 target 的属性,set 捕捉器用于写入 target 的属性,等等。

工作机理

proxy到底用来拦截什么?

对于对象的大多数操作,JavaScript 规范中有一个所谓的“内部方法”,它描述了最底层的工作方式。例如 [[Get]],用于读取属性的内部方法,[[Set]],用于写入属性的内部方法。

对每一个内部方法,都有一个对应的Proxy handler去拦截这些方法的调用:proxy规范

一个简单的举例

proxy

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
// target
const apartment = {
temperature: 22,
residents: ['Alice', 'Bob']
};

// 门卫
const apartmentProxy = new Proxy(apartment, {
// handler_1
get(target, prop) {
//log
console.log(`有人想要查看${prop}的信息`);
return target[prop];
},

// handler_2
set(target, prop, value) {
//log
console.log(`有人想要修改${prop}${value}`);
// 例如:只允许温度在18-30度之间
if (prop === 'temperature' && (value < 18 || value > 30)) {
console.log('温度设置被拒绝:超出合理范围!');
return false;
}
target[prop] = value;
return true;
}
});

Reflect

Intro

Reflect 是一个内建对象,前面所讲过的内部方法,例如 [[Get]][[Set]] 等,都只是规范性的,不能直接调用。Reflect 对象使调用这些内部方法成为了可能。对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有一个对应的方法,其名称和参数与 Proxy 捕捉器相同。

reflect

因此,我们可以通过 Reflect 将操作转发给原始对象。

Reflect有什么优势?

你可能会问,既然我们总是要调用内部方法的,为什么还要用Reflect在proxy中调这些方法呢?

这里以Reflect.get为例阐述其在处理继承关系的优势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let owner = {
_name: "Eason",
get name() {
return this._name;
}
};

let ownerProxy = new Proxy(owner, {
get(target, prop, receiver) {
return target[prop]; // (*) target = owner
}
});

let guest = {
__proto__: ownerProxy,
_name: "Guest"
};

// 期望输出:Guest
console.log(guest.name); // 实际输出:Eason

我们希望读取 guest.name返回 Guest,而不是 Eason,问题出在哪里呢?

分析:

  1. 读取 guest.name,由于 guest对象没有name属性,搜索转到其原型 ownerProxy
  2. 从代理读取name时,get handler触发,从owner对象返回 owner[prop]
  3. 此时的prop是一个getter,它将在 this=target上下文中返回 _name
  4. 如何把上下文传递给 getter?这本质其实是一个this指向问题,对一般的数据属性我们可以用call/apply/bind处理,但getter是访问器属性,它不能“被调用”,只能被访问。

解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let owner = {
_name: "Eason",
get name() {
return this._name;
}
};

let ownerProxy = new Proxy(owner, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
});

let guest = {
__proto__: ownerProxy,
_name: "Guest"
};

console.log(guest.name); // 实际输出:Guest

我们需要用到get handler的第三个参数 receiver,它保留了对正确this(Guest)的引用,通过 Reflect.get传递给getter

Proxy局限性

Vue

为突出逻辑,以下部分代码在源码的基础上有适当改动

Vue2

在初始化阶段,Vue2会对配置对象中的不同属性做相关处理

  • dataprops中的每个属性变成响应式属性,每个属性内部持有一个Dep依赖收集器
  • computed计算属性,内部创建computed watcher,每个computed watcher持有一个Dep依赖收集器
  • watch,内部创建user watcher,即用户自定义的一些watch,data or computed中的属性Dep会存储与自己相关的watcher

vue2

侦测数据变化

Observer

在Vue2中,利用 Observer类和 Object.defineProperty,通过劫持对象属性,实现侦测数据变化

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
export class Observer {
dep: Dep
vmCount: number // 根$data的Vue实例数量

constructor(public value: any, public shallow = false) {
this.dep = new Dep()
this.vmCount = 0

// 新增一个__ob__属性,标记此 value 已经变为响应式了
def(value, '__ob__', this)

if (isArray(value)) {
// type 是 Array 时
if (hasProto) {
// 如果浏览器支持__proto__,直接设置原型链
;(value as any).__proto__ = arrayMethods
} else {
// 不支持__proto__时,逐个复制方法
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
// type 是 Object 时,遍历其所有属性添加 get/set
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}


// 为Array的每一个元素创建 Observer
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false)
}
}
}

defineReacitve

对象的属性定义响应式,Vue2使用 defineReactive将一个对象转化成可观测对象

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
export function defineReactive(
obj: object,
key: string,
val?: any,
shallow?: boolean,
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// 参数处理,只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key];
}

// obj里嵌套对象,递归添加响应式,进行深度侦测
let childOb = shallow ? val && val.__ob__ : observe(val, false)

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,

get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val

// 收集 Watcher
dep.depend()

if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
// value 是 Array 时特殊处理
dependArray(value)
}
}
return isRef(value) && !shallow ? value.value : value
},

set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
val = newVal

// obj里嵌套对象,递归添加响应式,进行深度侦测
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false)

//通知 Wathcer 进行视图更新
dep.notify()
}
})

return dep
}

分析:

  1. 通过以上方法仅只能观测Object数据的取值getter和修改更新已有数据setter,为解决这一点,Vue2增加了两个全局API,Vue.set(vm.$set)Vue.delete(vm.$delete)
  2. Object.defineProperty 仅可以检测数组的下标变化(即通过下标获取某个元素和修改元素值),但无法检测数组长度变化,因此在 array.tspushunshiftsplice进行了特殊处理,也是为什么Observer需要特判
  3. Object.defineProperty需要遍历对象的每个属性,假如属性值也是对象,则需要递归地遍历,性能较低

依赖收集与更新

数据变的可观测后,我们可以知道数据什么时候发生了变化,那么现在的问题就是当某个数据变化后,我们该通知哪部分视图进行更新?

Dep

对象的每个属性都有一个Dep依赖管理器,其串联了属性和sub(或effect)

1
2
3
4
5
6
7
8
9
10
// Dep存储Watcher
Dep {
subs: [Watcher1, Watcher2, ...]
}

// Watcher存储Dep
Watcher {
deps: [Dep1, Dep2, ...],
depIds: Set(1, 2, ...) // 用于去重
}

当某个属性被访问,其通过depend收集访问它的watcher,当其数据变化,通过notify遍历更新所有依赖它的watcher

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
export default class Dep {
static target?: DepTarget | null
subs: Array<DepTarget | null>

constructor() {
this.subs = []
}

addSub(sub: DepTarget) {
this.subs.push(sub)
}

//省略remove

//收集依赖
depend() {
if (Dep.target) {
//dep.addSub(target)
Dep.target.addDep(this)
}
}

//派发更新
notify() {
// 浅拷贝
const subs = this.subs.filter(s => s) as DepTarget[]

// 遍历 Watcher update
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
sub.update()
}
}
}

Vue3

更新?

  1. Vue2中,响应式系统主要是为对象设计的,假如我们需要处理一个基本类型(Number,String…)的数据,需要将其包装在对象中。因此Vue3在reactive的基础上推出了ref,它提供了一种更自然的方式来直接处理任意类型。

  2. 使用 Proxy直接代理对象,直接监听整个对象

    1. 不需要重写array方法
    2. 可监听属性的新增和删除(不再使用set和delete)
  3. 相较于使用数组进行watcher的存储,采用 WeakMap<target, Map<key, Set<effect>>>结构,可自动垃圾回收

    vue3结构

数据代理

vue3

ref

ref函数本身很简单,就是直接调用 createRef

1
2
3
export function ref(value?: unknown) {
return createRef(value, false)
}

createRef

1
2
3
4
5
6
7
createRef`中对传入的rawValue进行判断,假如已经是响应式,直接返回,不是则作为参数传入`RefImpl
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}

RefImpl

RefImpl中维护一个 _value,其值就是平时我们调用.value获取ref的值;维护一个 _rawValue,用于和set的 newValue比较,判断是否触发 trigger进行响应式更新;维护一个Dep,记录依赖该属性的Subscriber(在Vue2中叫Watcher)

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
class RefImpl<T = any> {
_value: T
private _rawValue: T

dep: Dep = new Dep()

public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false

constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value)
this._value = isShallow ? value : toReactive(value)
this[ReactiveFlags.IS_SHALLOW] = isShallow
}

get value() {
this.dep.track()
return this._value
}

set value(newValue) {
const oldValue = this._rawValue
const useDirectValue =
this[ReactiveFlags.IS_SHALLOW] ||
isShallow(newValue) ||

newValue = useDirectValue ? newValue : toRaw(newValue)
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)

this.dep.trigger()
}
}
}
  • 当newValue是Proxy对象,调用 toRaw将其转成originalObj
  • _value调用 toReactive获得,由下面代码可看出其作用即根据value的type进行区别处理,若是Object,交给reactive处理,基础类型则直接返回,即处理对象之前加了一条分支判断
1
2
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value

createReactiveObject

到这其实就进入在Vue3中使用reactive声明变量的流程(即 const var = reactive({count: 1})),函数reactive在做完简单的可读性处理后直接调用 createReactiveObject,其核心逻辑就是为target创建一个Proxy对象,并根据 targetType传入不同的 handler

targetType

  • COMMON:Object、Array
  • COLLECTION:Map、Set、WeakMap、WeakSet
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: Target,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
if (!isObject(target)) {
return target
}

//target本身是Proxy(与源码有出入,以逻辑为主)
if (target instanceof Proxy) {
return target
}

//target已经存在Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}

//类型处理
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
}

依赖收集与更新

和Vue2一样,Vue3同样通过track进行依赖收集,trigger进行派发更新,只不过二者是在handler执行

Vue2通过数组实现属性和watcher之间之间的双向依赖收集,而Vue3采用链表实现双向链接,进一步降低了依赖清理的时间复杂度(O(n)–>O(1)),提高了依赖管理的性能

1
2
3
4
5
6
7
8
Link {
sub: Effect, // 指向effect
dep: Dep, // 指向dep
nextDep: Link, // effect的下一个依赖
prevDep: Link, // effect的上一个依赖
nextSub: Link, // dep的下一个订阅者
prevSub: Link // dep的上一个订阅者
}

Effect

我们以下涉及多层 effect 嵌套例子为例,走一遍依赖收集与更新的流程:

1
2
3
4
5
6
7
const count = ref(0)
const double = computed(() => count.value * 2)
const triple = computed(() => double.value * 1.5)

watchEffect(() => {
console.log(`Triple value is: ${triple.value}`)
})

effect是subscriber接口的具体实现,其中包含了批处理机制、依赖清理等逻辑,其核心 ReactiveEffect的主要逻辑为 run方法,其余与生命周期,自定义调度等相关的属性和方法这里不做进一步阐述

  1. 首先,watchEffect创建了一个 ReactiveEffect,double和triple同理,当执行watchEffect时,调用其 run方法:
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
run(): T {
//清理之前的依赖
cleanupEffect(this)

//准备新的依赖
prepareDeps(this)

//保存 watchEffect 上下文
const prevEffect = activeSub //此时是null
const prevShouldTrack = shouldTrack

//设置 watchEffect 为当前 active 的 effect
activeSub = this
shouldTrack = true

try {
return this.fn() //console.log(`Triple value is: ${triple.value}`)

//开始嵌套...
} finally {
//清理 watchEffect 的依赖
cleanupDeps(this)
//现场恢复
activeSub = prevEffect
shouldTrack = prevShouldTrack
}
}
  • Vue3使用全局变量来跟踪当前正在执行的effect:
1
2
export let activeSub: Subscriber | undefined 
export let shouldTrack = true
  • 由run代码可以看出,当一个effect开始执行时,会将自己设置为activeSub(当前活跃的effect)
  • 每次run的时候都需要清理之前的依赖,因为每次所需的依赖可能有区别
  • 当访问triple.value时,其还未计算过值,触发triple的effect运行,double同理,给出effect嵌套执行依赖和执行栈变化:
    • count -> double -> triple -> watchEffect
    • 执行栈变化:
      1. 初始:activeSub: nullshouldTrack: true
      2. watchEffect:activeSub: watchEffectInstanceshouldTrack: true
      3. triple:activeSub: tripleEffectInstanceshouldTrack: true
      4. double:activeSub: doubleEffectInstanceshouldTrack: true
  • 通过保存和恢复上下文(activeSub & shouldTrack),可以很好的处理嵌套effect
  1. 对于每一个正在执行的effect,由于将自己暴露为全局变量activeSub,其依赖可以在track的时候轻松获取
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
// dep.ts
export function track(target: object, key: unknown): void {
if (shouldTrack && activeSub) {
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 Dep()))

//用于依赖清理
dep.map = depsMap
dep.key = key
}
dep.track()
}
}
// dep.ts
// dep.track()
track(): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
}

let link = this.activeLink
if (link === undefined || link.sub !== activeSub) {
link = this.activeLink = new Link(activeSub, this)

// effect收集依赖
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}

//dep收集依赖——link.dep.subs.nextSub = link
addSub(link)
} else if (link.version === -1) {
//重用已存在的Link对象,减少内存分配和gc
link.version = this.version

//重排,依赖列表按最新访问顺序
if (link.nextDep) {
const next = link.nextDep
next.prevDep = link.prevDep
if (link.prevDep) {
link.prevDep.nextDep = next
}

link.prevDep = activeSub.depsTail
link.nextDep = undefined
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link

if (activeSub.deps === link) {
activeSub.deps = next
}
}
}
return link
}
  1. 假设此时更新了 count.value = 1,此时触发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
// dep.ts
export function trigger(
target: object,
key?: unknown,
): void {
const depsMap = targetMap.get(target)
if (!depsMap) return

startBatch() // 开始批量更新

// 获取依赖该属性的effect
const dep = depsMap.get(key)
if (dep) {
dep.trigger() // 触发更新
}

endBatch() // 结束批量更新
}
// dep.ts
trigger(): void {
// 版本管理...

this.notify()
}

notify(): void {
startBatch()
try {
for (let link = this.subs; link; link = link.prevSub) {
if (link.sub.notify()) {
// 对computed优先调用notify,而不是在computed内调用notify,减少递归栈
;(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
endBatch()
}
}
  • 这里对dep的subs进行逆序遍历,即watchEffect->triple->double,主要为了配合批处理机制(batch processing):
    • 假如我们正向遍历,更新double的时候会触发triple更新,triple又会触发watchEffect更新…如此往复需要(On^2)复杂度才可更新完嵌套执行的effect
    • 而逆序遍历,先触发watchEffect更新,维护一个链表,采用头插法不断往其中加入effect,最后只需要On从头遍历一遍该链表即可完成嵌套effect的更新,且保证更新顺序按照subs原始的存储顺序更新
  • 由以上代码可得出computed会被优先处理,再处理普通effect
  • 至此,此时double被更新成2,triple更新成3,打印Triple value is: 3

参考资料

Vue2源码:https://github.com/vuejs/vue

Vue3源码:https://github.com/vuejs/core

Vue官方文档:https://cn.vuejs.org/guide/extras/reactivity-in-depth#how-reactivity-works-in-vue

特别鸣谢web某小动物:https://www.cheems.life/blog/65