非原始值的响应式方案(四)

合理触发响应

在非原始值的响应式方案(三)这一章中我们处理了很多边界条件。比如我们需要明确知道操作的类型是'ADD'还是'SET',或者是其他操作类型,从而正确的触发响应。但是想要合理的触发响应,还有许多工作要做。

首先我们来看第一个问题,即当值没有发生变化时,应该不需要触发响应才对:

1
2
3
4
5
6
7
8
9
10
const obj = { name: '张三' }
const p = new Proxy(obj, {
/** ... */
})

effect(() => {
console.log(p.name)
})

p.name = '张三'

如上面的代码所示,p.name的初始值为’张三’,当为p.name设置新的值时,如果值没有发生变化时则不需要触发响应。为了满足需求,我们需要修改set拦截函数的代码,在调用trigger函数触发响应之前,需要检查值是否真的发生了变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const p = new Proxy(obj, {
set(target, key, newVal, receiver) {
// 先获取旧值
const oldVal = target[key]

// 看看操作类型是设置已有属性还是添加新属性
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
// 拿到执行结果
const res = Reflect.set(target, key, newVal, receiver)
// 比较新值与旧值 只要当不全等的时候才会触发响应
if (oldVal !== newVal) {
trigger(target, key, type)
}

return res
}
})

如上面代码所示,我们在 set 拦截函数内首先获取旧值 oldVal, 接着比较新值与旧值,只有当他们不完全相等时才会触发响应。现在,如果我们再次测试开头的那个例子,会发现重新设置相同的值已经不会触发响应了。

然而,仅仅进行全等比较是有缺陷的,这体现在对NaN的处理上。我们知道NaNNaN进行全等比较时总会得到 false:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NaN === NaN // false
NaN !== NaN // true

const obj = { foo: NaN }
const p = new Proxy(obj, {
set(target, key, newVal, receiver) {
// 先获取旧值
const oldVal = target[key]

// 看看操作类型是设置已有属性还是添加新属性
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
// 拿到执行结果
const res = Reflect.set(target, key, newVal, receiver)
// 比较新值与旧值 只要当不全等的时候才会触发响应 并且都不是NaN的时候才触发响应
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}

return res
}
})

但是要合理的触发响应,仅仅处理关于NaN的问题还不够。我们接下来讨论一种从原型上继承属性的情况。我们需要封装一个reactive函数,该函数接收一个对象作为参数,并返回为其创建的响应式数据:

1
2
3
4
5
function reactive(obj) {
return new Proxy(obj, {
// 省略前文讲解的拦截函数
})
}

可以看到reactive只是对Proxy进行了一层封装。接下来,我们基于reactive创建一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
const obj = { foo: 1 }
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
// 使用parent作为child的原型
Object.setPrototypeOf(child, parent)

effect(() => {
console.log(child.bar) // 1
})
// 修改child.bar的值
child.bar = 2 // 会导致副作用函数重新执行两次

观察如上代码,我们定义了空对象objproto,分别为二者创建了对应的响应式数据childparent,并且使用Object.setPrototypeOf方法将parent设置为child的原型。接着,在副作用函数中访问 child.bar 的值。从代码中可以看出,child 本身没有 bar 属性,因此在访问 child.bar 时,值是从原型上继承而来的。但无论如何,child 既然是响应式数据,那它与副作用函数之间就会建立联系,因此当我们执行child.bar = 2时,期望副作用函数会重新执行。但是如果你执行了上面的代码,会发现副作用函数不仅执行了,还执行了两次,这会造成不必要的更新。

为了搞清楚问题的原因,我们需要逐步分析整个过程。当在副作用函数中读取child.bar的值时,会触发child代理对象的 get 拦截函数。我们知道在拦截函数内是使用Reflect.get(target, key, receiver)来得到最终结果的,对应上面这个例子,这句话相当于:

1
Reflect.get(target, key, receiver)

这其实是实现了通过 obj.bar 来访问属性值的默认行为。也就是说引擎内部通过调用obj对象所部署的[[Get]]内部方法来得到最终结果的。因此,我们有必要查看规范 10.1.8.1 节来了解[[Get]]内部方法的执行流程。通过查看流程得知,如果对象自身不存在该属性,那么会获取对象的原型,并调用原型的[[Get]]方法的到最终结果。对应到上面的例子中,child.bar 属性值时,由于对象 child 代理的对象 obj 自身没有 bar 属性,因此会读取对象 obj 的原型,也就是 parent 对象,所以最终得到的实际上是 parent.bar 的值。但是不要忘记,parent 本身也是一个响应式数据,所以在副作用函数中访问 parent.bar 的值时,会导致副作用函数被收集,从而建立响应联系。所以我们可以得出一个结论:child.bar 和 parent.bar 都与副作用函数建立了响应联系。

但是这仍然解释不了为什么当设置 child.bar 的值时,会连续触发两次副作用函数执行,所以接下来我们要看看当设置操作发生时的具体执行流程。我们知道,当执行 child.bar = 2 时,会调用 child 代理对象的 set 拦截函数。同样,在 set 拦截函数内,我们使用Reflect.set(target, key, newVal, receiver)来完成默认的设置行为,即引擎会调用 obj 对象部署的[[Set]]内部方法,根据规范的 10.1.9.2 节可知[[Set]]内部方法的执行流程。通过流程 2 得知,如果设置的属性不存在于对象上,那么会取得其原型,并调用原型的[[Set]]方法,也就是 parent 的[[Set]]内部方法,由于 parent 是代理对象,所以这就相当于执行了它的 set 拦截函数。前面我们分析过,当读取 child.bar 时,副作用函数不仅会被 child.bar 收集,也会被 parent.bar 收集。所以当 parent 代理对象的 set 拦截函数执行时,就会触发副作用函数重新执行,这就是为什么修改 child.bar 会导致副作用函数重新执行两次。

知道了问题所在,那么就需要思考解决方案。思路也很简单,既然执行两次,那么我们就屏蔽其中一次不就可以了嘛?我们可以把由 parent.bar 触发的那次副作用函数的重新执行屏蔽。怎么屏蔽呢?要知道,两次更新是由于 set 拦截函数被触发了两次导致的,所以我们只要能够在 set 拦截函数内部区分这两次更新就可以了。当我们设置 child.bar 的值时,会执行 child 代理对的 set 拦截函数:

1
2
3
4
set(target, key, newVal, receiver) {
// target是原始对象obj
// receiver是代理对象child
}

此时的 target 是原始对象 obj,receiver 是代理对象 child,我们发现 receiver 其实就是 target 的代理对象。

但由于 obj 上不存在 bar 属性,所以会取得 obj 的原型 parent,并执行 parent 代理对象的 set 拦截函数:

1
2
3
4
set(target, key, newVal, receiver) {
// target是原始对象proto
// receiver是代理对象child
}

我们发现呢,此时 target 是原始对象 proto,而 receiver 仍然是代理对象 child,不再是 target 的代理对象。通过这个特点,我们可以看到 targetreceiver 的区别。由于我们最初设置的是 child.bar 的值,所以无论在什么情况下,receiver 都是 child,而 target 则是变化的。根据这个区别,我们很容易找到解决办法,只需要判断 receiver 是否是 target 的代理对象即可。只有当 receivertarget的代理对象时才触发更新,这样就能够屏蔽由原型引起的更新了。

所以接下来的问题就变成了如何确定 receiver 是不是 target 的代理对象,这需要我们为 get 拦截函数添加一个能力,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 代理对象可以通过raw属性访问原始数据
if (key === 'raw') {
return target
}
track(target, key)
return Reflect.get(target, key, receiver)
}
})
}

有了它,我们就能够在 set 拦截函数中判断 receiver 是不是 target 的代理对象了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function reactive(obj) {
return new Proxy(obj, {
set(target, key, newVal, receiver) {
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
const res = Reflect.get(target, key, newVal, receiver)

// target === receiver.raw 说明receiver是target的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
}
})
}

如上代码所示,我们新增了一个判断条件,只有当receivertarget的代理对象时才能触发更新,这样就能屏蔽由原型引起的更新,从而避免不必要的更新操作。

代码

至此,我们已经写了不少代码了,为了便于理解,将之前写好的代码整理一下。

清除依赖工具函数

1
2
3
4
5
6
7
8
// 清除依赖函数
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}

副作用函数

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
// 副作用函数栈
const effectStack = []
// 当前执行的副作用函数
let activeEffect

// 副作用函数
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
// 将options挂载到effectFn上
effectFn.options = options
effectFn.deps = []
// 只有非lazy的情况下才会执行
if (!options.lazy) {
effectFn()
}
// 将副作用函数作为返回值返回
return effectFn
}

计算属性

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 computed(getter) {
// value用来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true则意味着脏,需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
if (!dirty) {
dirty = true
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应
trigger(obj, 'value')
}
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
// 读取value时,手动调用track进行追踪
track(obj, 'value')
return value
}
}
return obj
}

侦听器

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
// 侦听器工具函数 用来深度监听
function traverse(value, seen = new Set()) {
// 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 假设value就是一个对象,使用for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const k in value) {
traverse(value[k], seen)
}
return value
}

// 侦听器
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值与新值
let oldValue, newValue
// 用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
const job = () => {
newValue = effectFn()
// 在调用回调函数之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 将onInvalidate作为回到函数的第三个参数,供用户使用
cb(newValue, oldValue, onInvalidate)
oldValue = newValue
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: () => {
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
})
if (options.immediate) {
job()
} else {
// 手动调用副作用函数,拿到的就是旧值
oldValue = effectFn()
}
}

依赖收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明一个桶 用来存储副作用函数
const bucket = new WeakMap()
// 依赖收集
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
// deps就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到activeEffect.deps数组中 完成依赖收集
activeEffect.deps.push(deps)
}

派发更新

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
// 触发的操作类型
const TriggerType = {
SET: 'SET',
ADD: 'ADD',
DELETE: 'DELETE'
}
// 拦截ownKeys时使用的作为追踪的key
const ITERATE_KEY = Symbol()
function trigger(target, key, type) {
// 从桶里拿出来与target相关的依赖集合
const depsMap = bucket.get(target)
if (!depsMap) return
// 找到与当前key相关的副作用函数们
const effects = depsMap.get(key)
// 声明一个Set结构的集合
const effectsToRun = new Set()
// 将与key相关的副作用函数们添加进effectsToRun
effects &&
effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
if (type === TriggerType.ADD || type === TriggerType.DELETE) {
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与ITERATE_KEY相关的副作用函数们添加进effectsToRun
iterateEffects &&
iterateEffects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}

effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数有调度器,那么优先调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否则直接执行副作用函数,默认行为
effectFn()
}
})
}

刷新任务队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用Promise.resolve()创建一个promise实例,用它将一个任务添加进微任务队列
const p = Promise.resolve()
// 标志位代表是否正在刷新队列
let isFlushing = false
// 刷新任务队列
function flushJob() {
// 如果当前处于刷新队列状态,则什么都不做
if (isFlushing) {
return
}
// 设置为true 标明正在处于刷新队列状态
isFlushing = true
// 在微任务队列中刷新队列
p.then(() => {
jobQueue.forEach((job) => job())
}).finally(() => {
// 结束后重置isFlushing
isFlushing = false
})
}

将一个普通对象转为响应式数据

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
// 将一个普通对象转为响应式数据
function reactive(obj) {
return new Proxy(obj, {
// 拦截属性访问 obj.name
get(target, key, receiver) {
// 代理对象可以通过raw属性访问原始数据
if (key === 'raw') {
return target
}
track(target, key)
return Reflect.get(target, key, receiver)
},
// 拦截in操作符 'name' in p
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newVal, receiver) {
// 先获取旧值
const oldVal = target[key]
// type看看操作类型是添加新属性还是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
// 拿到执行结果
const res = Reflect.set(target, key, newVal, receiver)
// 如果两者相等,说明receiver是target的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 检查被操作的属性是否是对象自身的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)

if (res && hadKey) {
// 只有当被删除的属性是自身属性且成功被删除时,才触发更新
trigger(target, key, 'DELETE')
}
// 返回结果
return res
}
})
}