优化
# 前言
在前面的文章中,我们一步一步实现了reactive
和ref
两个响应式的API,基本功能是实现了。但其实还有很多需要优化的地方,Vue3性能的大幅提升,其实也是在体现在这些细节的优化中。我们先来看下,就我们目前的实现来说,有哪些需要优化的地方。
# 避免重复代理
这个场景应该很容易想到,已经被代理过的对象再次使用ref
和reactive
代理时不应该再走代理的逻辑,因为该对象已经被代理了。举个例子:
const product = {
price: 10,
quantity: 20
}
// 第一次代理
const proxyProduct = reactive(product)
// 再次代理
const proxyProduct1 = reactive(product)
2
3
4
5
6
7
8
9
10
此时我们的proxyProduct
和proxyProduct2
不应该是不同的代理对象,我们希望相同的对象只被代理一次。我们可以使用这样的策略避免重复代理:
- 将之前已经代理过的对象缓存起来,再次代理时判断该对象是否在缓存中,如果是,就直接从缓存中取出代理,返回。反之,则走正常代理的逻辑。
明确了思路,我们用代码来一步步实现。
首先使用一个容器缓存代理对象,这里我们选择使用WeakMap
:
// 缓存已经代理过的对象
const proxtMap = new WeakMap()
2
然后在代理函数reactive
中进行判断:
function reactive(target) {
// 新增 已经代理的直接返回 proxy
const existProxy = proxtMap.get(target)
if (existProxy) {
return existProxy
}
const handler = {
get: function(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
},
set: function(target, key, value) {
let oldValue = target[key]
let res = Reflect.set(target, key, value)
// 这里触发依赖
if (res && oldValue !== value) {
trigger(target, key)
}
return res
}
}
const proxy = new Proxy(target, handler)
// 新增 之前没有代理过 将新代理的对象缓存起来
proxtMap.set(target, proxy)
return proxy
}
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
上述新增的逻辑很简单,其实就是在走代理逻辑之前,查看proxyMap
中是否缓存了该对象的代理对象,有就直接取出返回。否则,说明没有被代理过,走正常的代理逻辑,然后将代理对象缓存到proxyMap
中。
# effect 嵌套使用
这种场景在实际应用场景中也是存在的,举个例子:
effect(function fn1(){
effect(function fn2() { // activeEffect = effect2
total = product.price * product.quantity
})
total1 = goods.price * goods.quantity
})
2
3
4
5
6
我们使用了effect
嵌套了另一个effect
,即函数fn1
嵌套了函数fn2
。我们可以分析下这个函数是如何执行的,分析之前,我们再回顾一下effect
函数和track
函数的实现:
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
// 依赖收集函数
function track(target, key) {
if (!activeEffect) return
let depsMap = targrtMap.get(target)
if (!depsMap) {
targrtMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 注意这里收集了当前的 activeEffect
dep.add(activeEffect)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
好,我们分析一下执行的过程:首先是执行fn1
函数,此时activeEffect
等于fn1
,然后由于fn2
函数并没有异步操作,所以会按照顺序执行。也就是紧接着执行fn2
函数,此时activeEffect
等于fn2
,接着fn2
函数内计算了total
变量,此时会读取product
的price
属性和quantity
属性,从而触发依赖收集函数track
,product
对象的price
和quantity
的依赖都是fn2
,接着内部的effect
执行完毕,此时activeEffect
为null。接着继续执行外部effect
的逻辑,也就是执行:
total1 = goods.price * goods.quantity
此时,想必大家都发现问题了。我们此时的activeEffect
已经是null
,这里即使读取了属性,也收集不到依赖。这就是问题的所在。那我们的期望是什么呢?
我们希望执行到外部effect
的时候,activeEffect
等于fn1
,这样依赖才能被正确的收集到。如何解决这样的问题呢?其实,大家仔细想想,这像不像我们函数的调用栈,首先入栈的函数先执行,执行完就出栈,栈顶的元素永远是我们当前需要执行的函数。是的,Vue3中就是这样解决这个问题的。我们来看下具体如何实现。
首先定义一个栈:
// 使用一个栈结构处理effect嵌套的情况
const effectStack = []
2
然后,在effect
函数中将当前执行的activeEffect
推入栈,默认执行一次后,将栈顶元素出栈,并将activeEffect
始终指向为我们栈顶的元素。
function effect(fn) {
activeEffect = fn
if (!effectStack.includes(activeEffect)) {
effectStack.push(activeEffect)
try {
return activeEffect()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
2
3
4
5
6
7
8
9
10
11
12
再回到我们刚才嵌套effect
的例子,我们再分析一下,看是否符合我们的期望:
effect(function fn1(){
effect(function fn2() {
total = product.price * product.quantity
})
total1 = goods.price * goods.quantity
})
2
3
4
5
6
首先执行fn1
函数,此时activeEffect
等于fn1
然后将fn1
推入effectStack
。接着执行内部的effect
函数,此时activeEffect
等于fn2
,同时会将fn2
推入effectStack
,此时effectStack
的元素如图:
接着fn2
函数内计算了total
变量,此时会读取product
的price
属性和quantity
属性,从而触发依赖收集函数track
,product
对象的price
和quantity
的依赖都是fn2
,接着内部的effect
执行完毕。注意,内部effect
函数执行完毕后,会删除栈顶的元素,也就是删除栈顶的fn2
,并将activeEffect
指向栈顶的元素,因为我们此时栈顶的元素是fn1
,也就是说此时的activeEffect
等于fn1
:
这样我们就解决了依赖嵌套的问题,完整代码如下:
// 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
const targrtMap = new WeakMap()
// 缓存已经代理过的对象
const proxtMap = new WeakMap()
// 使用一个栈结构处理effect嵌套的情况
const effectStack = []
// 使用 activeEffect 变量保存当前激活的 effect
let activeEffect = null
// 引用类型的代理函数
function reactive(target) {
// 已经代理的直接返回 proxy
const existProxy = proxtMap.get(target)
if (existProxy) {
return existProxy
}
const handler = {
get: function(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
},
set: function(target, key, value) {
let oldValue = target[key]
let res = Reflect.set(target, key, value)
// 这里触发依赖
if (res && oldValue !== value) {
trigger(target, key)
}
return res
}
}
const proxy = new Proxy(target, handler)
proxtMap.set(target, proxy)
return proxy
}
// 基本类型的响应式API 类似于 Object.defineProperty
function ref(raw) {
const target = {
get value() {
console.log('基本类型----收集依赖')
track(target, 'value')
return raw
},
set value(newValue) {
console.log('基本类型----触发依赖')
raw = newValue
trigger(target, 'value')
}
}
return target
}
// 依赖收集函数
function track(target, key) {
if (!activeEffect) return
let depsMap = targrtMap.get(target)
if (!depsMap) {
targrtMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 收集依赖
dep.add(activeEffect)
}
// 触发依赖函数
function trigger(target, key) {
let depsMap = targrtMap.get(target)
if (!depsMap) return
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
// 副作用函数
function effect(fn) {
activeEffect = fn
if (!effectStack.includes(activeEffect)) {
effectStack.push(activeEffect)
try {
return activeEffect()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 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
89
90
91
92
93
94
95
96
97
98
99
然后写个测试例子测试下:
// 测试例子
let total = 0
let total1 = 0
let discount = 0.8
let product = {
price: 10,
quantity: 2
}
let goods = {
price: 10,
quantity: 2
}
const proxyProduct = reactive(product)
const proxyProduct1 = reactive(goods)
effect(() => {
effect(() => {
total = proxyProduct.price * proxyProduct.quantity
})
total1 = proxyProduct1.price * proxyProduct1.quantity
})
// 所以此时total的值应该是400
proxyProduct.price = 200
console.log(total) // 400
// 所以此时total1的值应该是800
proxyProduct1.price = 400
console.log(total1) // 800
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
# 值改变时,才触发依赖函数
我们先回顾下,我们实现的reactive
函数,我们只保留了需要关注的代码部分:
function reactive(target) {
// ...
const handler = {
set: function(target, key, value) {
let res = Reflect.set(target, key, value)
// 这里触发依赖
trigger(target, key)
return res
}
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
请注意我们执行trigger
函数的时机,其实是只要有值改变,我们就会执行当前的trigger
函数,当是如果是下面这种场景呢?
const p = {
price: 20,
quantity: 2
}
const product = reactive(p)
effect(() => {
console.log(product.price)
})
product.price = 10
product.price = 10
2
3
4
5
6
7
8
9
10
11
12
13
按照我们当前的实现,我们修改了两次price
的值,就会打印两次10
。但这符合我们的期望吗?并不符合。当前price
的值相比于第一次修改后并没有改变,此时并不应该再次执行依赖函数。所以,这里我们可以做个优化:只有当值真正改变后,我们才执行trigger
函数,我们来实现下:
function reactive(target) {
// ...
const handler = {
set: function(target, key, value) {
let oldValue = target[key]
let res = Reflect.set(target, key, value)
if (res && oldValue !== value) {
trigger(target, key)
}
return res
}
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们会把之前的值和即将要修改的值进行对比,如果不相等,我们才会去执行依赖函数,这样就避免了重复执行依赖函数的问题。 最终实现的完整代码如下:
// 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
const targrtMap = new WeakMap()
// 缓存已经代理过的对象
const proxtMap = new WeakMap()
// 使用一个栈结构处理effect嵌套的情况
const effectStack = []
// 使用 activeEffect 变量保存当前激活的 effect
let activeEffect = null
// 引用类型的代理函数
function reactive(target) {
// 已经代理的直接返回 proxy
const existProxy = proxtMap.get(target)
if (existProxy) {
return existProxy
}
const handler = {
get: function(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
},
set: function(target, key, value) {
let oldValue = target[key]
let res = Reflect.set(target, key, value)
// 这里触发依赖
if (res && oldValue !== value) {
trigger(target, key)
}
return res
}
}
const proxy = new Proxy(target, handler)
proxtMap.set(target, proxy)
return proxy
}
// 基本类型的响应式API 类似于 Object.defineProperty
function ref(raw) {
const target = {
get value() {
console.log('基本类型----收集依赖')
track(target, 'value')
return raw
},
set value(newValue) {
console.log('基本类型----触发依赖')
raw = newValue
trigger(target, 'value')
}
}
return target
}
// 依赖收集函数
function track(target, key) {
if (!activeEffect) return
let depsMap = targrtMap.get(target)
if (!depsMap) {
targrtMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 收集依赖
dep.add(activeEffect)
}
// 触发依赖函数
function trigger(target, key) {
let depsMap = targrtMap.get(target)
if (!depsMap) return
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
// 副作用函数
function effect(fn) {
activeEffect = fn
if (!effectStack.includes(activeEffect)) {
effectStack.push(activeEffect)
try {
return activeEffect()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 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
89
90
91
92
93
94
95
96
97
98