通过 vue3 学习响应系统的设计思想
响应系统的作用与实现
响应式数据与副作用函数
副作用函数
指的是会产生副作用的函数
一个 响应式数据
最基本的实现依赖于 对“读取”和“设置”操作的拦截
,从而在副作用函数与响应式数据之间建立联系。
响应系统的根本实现原理:
- 当“读取”操作发生时,将当前执行的副作用函数存储到“桶”中;
- 当“设置”操作发生时,再将副作用函数从“桶”里取出并执行。
响应式数据的基本实现
能拦截一个对象的读取和设置操作
就能做到响应式了。
Vue2 通过 Object.defineProperty
函数实现了响应式。
Vue3 通过 使用 代理对象 Proxy
来实现了响应式。
Proxy 提供了更加灵活和强大的拦截能力,可以监听对象的任何属性变化,包括新增和删除属性。
1// 存储副作用函数的桶
2const bucket = new Set()
3
4// 原始数据
5const data = { text: 'hello world' }
6// 对原始数据的代理
7const obj = new Proxy(data, {
8 // 拦截读取操作
9 get(target, key) {
10 // 将副作用函数 effect 添加到存储副作用函数的桶中
11 bucket.add(effect)
12 // 返回属性值
13 return target[key]
14 },
15 // 拦截设置操作
16 set(target, key, newVal) {
17 // 设置属性值
18 target[key] = newVal
19 // 把副作用函数从桶里取出并执行
20 bucket.forEach(fn => fn())
21 }
22})
23
24function effect() {
25 document.body.innerText = obj.text
26}
27effect()
28
设计一个完善的响应系统
WeakMap 对 key 是弱引用,不影响垃圾回收器的工作
1// 存储副作用函数的桶
2const bucket = new WeakMap()
3
4// 原始数据
5const data = { text: 'hello world' }
6// 对原始数据的代理
7const obj = new Proxy(data, {
8 // 拦截读取操作
9 get(target, key) {
10 // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
11 track(target, key)
12 // 返回属性值
13 return target[key]
14 },
15 // 拦截设置操作
16 set(target, key, newVal) {
17 // 设置属性值
18 target[key] = newVal
19 // 把副作用函数从桶里取出并执行
20 trigger(target, key)
21 }
22})
23
24// 在 get 拦截函数内调用 track 函数追踪变化
25function track(target, key) {
26 // 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key -->effects
27 let depsMap = bucket.get(target)
28 // 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
29 if (!depsMap) {
30 bucket.set(target, (depsMap = new Map()))
31 }
32 // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
33 // 里面存储着所有与当前 key 相关联的副作用函数:effects
34 let deps = depsMap.get(key)
35 // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
36 if (!deps) {
37 depsMap.set(key, (deps = new Set()))
38 }
39 // 最后将当前激活的副作用函数添加到“桶”里
40 deps.add(activeEffect)
41}
42
43// 在 set 拦截函数内调用 trigger 函数触发变化
44function trigger(target, key) {
45 // 根据 target 从桶中取得 depsMap,它是 key --> effects
46 const depsMap = bucket.get(target)
47 if (!depsMap) return
48 // 根据 key 取得所有副作用函数 effects
49 const effects = depsMap.get(key)
50 // 执行副作用函数
51 effects && effects.forEach(fn => fn())
52}
53
54// 用一个全局变量存储当前激活的 effect 函数
55let activeEffect
56function effect(fn) {
57 // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
58 activeEffect = fn
59 // 执行副作用函数
60 fn()
61}
62
63effect(() => {
64 console.log('effect run')
65 document.body.innerText = obj.text
66})
67
68setTimeout(() => {
69 trigger(data, 'text')
70}, 1000)
71
分支切换与 cleanup
1// 存储副作用函数的桶
2const bucket = new WeakMap()
3
4// 原始数据
5const data = { ok: true, text: 'hello world' }
6// 对原始数据的代理
7const obj = new Proxy(data, {
8 // 拦截读取操作
9 get(target, key) {
10 // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
11 track(target, key)
12 // 返回属性值
13 return target[key]
14 },
15 // 拦截设置操作
16 set(target, key, newVal) {
17 // 设置属性值
18 target[key] = newVal
19 // 把副作用函数从桶里取出并执行
20 trigger(target, key)
21 }
22})
23
24function track(target, key) {
25 let depsMap = bucket.get(target)
26 if (!depsMap) {
27 bucket.set(target, (depsMap = new Map()))
28 }
29 let deps = depsMap.get(key)
30 if (!deps) {
31 depsMap.set(key, (deps = new Set()))
32 }
33 deps.add(activeEffect)
34
35 // deps 就是一个与当前副作用函数存在联系的依赖集合
36 // 将其添加到 activeEffect.deps 数组中
37 // 新增
38 activeEffect.deps.push(deps)
39}
40
41function trigger(target, key) {
42 const depsMap = bucket.get(target)
43 if (!depsMap) return
44 const effects = depsMap.get(key)
45
46 // 新增
47 const effectsToRun = new Set()
48 effects && effects.forEach(effectFn => effectsToRun.add(effectFn))
49 effectsToRun.forEach(effectFn => effectFn())
50 // effects && effects.forEach(effectFn => effectFn())
51}
52
53// 用一个全局变量存储当前激活的 effect 函数
54let activeEffect
55function effect(fn) {
56 const effectFn = () => {
57 // 调用 cleanup 函数完成清除工作
58 // 新增
59 cleanup(effectFn)
60 // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
61 activeEffect = effectFn
62 fn()
63 }
64 // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
65 effectFn.deps = []
66 // 执行副作用函数
67 effectFn()
68}
69
70function cleanup(effectFn) {
71 for (let i = 0; i < effectFn.deps.length; i++) {
72 // deps 是依赖集合
73 const deps = effectFn.deps[i]
74 // 将 effectFn 从依赖集合中移除
75 deps.delete(effectFn)
76 }
77 // 最后需要重置 effectFn.deps 数组
78 effectFn.deps.length = 0
79}
80
81effect(() => {
82 console.log('effect run')
83 document.body.innerText = obj.ok ? obj.text : 'not'
84})
85
86setTimeout(() => {
87 obj.ok = false
88 setTimeout(() => {
89 obj.text = 'hello vue3'
90 }, 1000)
91}, 1000)
92
嵌套的 effect 与 effect 栈
1// 用一个全局变量存储当前激活的 effect 函数
2let activeEffect
3// effect 栈
4const effectStack = []
5
6function effect(fn) {
7 const effectFn = () => {
8 cleanup(effectFn)
9 // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
10 activeEffect = effectFn
11 // 在调用副作用函数之前将当前副作用函数压栈
12 effectStack.push(effectFn)
13 fn()
14 // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
15 effectStack.pop()
16 activeEffect = effectStack[effectStack.length - 1]
17 }
18 // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
19 effectFn.deps = []
20 // 执行副作用函数
21 effectFn()
22}
23
避免无限递归循环
在 trigger 动作发生时增加守卫条件: 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
1function trigger(target, key) {
2 const depsMap = bucket.get(target)
3 if (!depsMap) return
4 const effects = depsMap.get(key)
5
6 const effectsToRun = new Set()
7 effects && effects.forEach(effectFn => {
8 // 如果 trigger 触发执行的副作用函数
9 // 与当前正在执行的副作用函数相同,则不触发执行
10 if (effectFn !== activeEffect) {
11 effectsToRun.add(effectFn)
12 }
13 })
14 effectsToRun.forEach(effectFn => effectFn())
15 // effects && effects.forEach(effectFn => effectFn())
16}
17
调度执行
可调度性是响应系统非常重要的特性。
所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用 函数执行的时机、次数以及方式
。
1function effect(fn, options = {}) {
2 const effectFn = () => {
3 cleanup(effectFn)
4 // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
5 activeEffect = effectFn
6 // 在调用副作用函数之前将当前副作用函数压栈
7 effectStack.push(effectFn)
8 fn()
9 // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
10 effectStack.pop()
11 activeEffect = effectStack[effectStack.length - 1]
12 }
13
14 // 将 options 挂在到 effectFn 上
15 // 新增
16 effectFn.options = options
17
18 // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
19 effectFn.deps = []
20 // 执行副作用函数
21 effectFn()
22}
23
1function trigger(target, key) {
2 const depsMap = bucket.get(target)
3 if (!depsMap) return
4 const effects = depsMap.get(key)
5
6 const effectsToRun = new Set()
7 effects && effects.forEach(effectFn => {
8 if (effectFn !== activeEffect) {
9 effectsToRun.add(effectFn)
10 }
11 })
12 effectsToRun.forEach(effectFn => {
13 // 如果一个副作用函数存在调度器,则调用该调度器,
14 // 并将副作用函数作为参数传递
15 if (effectFn.options.scheduler) {
16 effectFn.options.scheduler(effectFn)
17 } else {
18 // 否则直接执行副作用函数(之前的默认行为)
19 effectFn()
20 }
21 })
22 // effects && effects.forEach(effectFn => effectFn())
23}
24
计算属性 computed 与 lazy
1function effect(fn, options = {}) {
2 const effectFn = () => {
3 cleanup(effectFn)
4 // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
5 activeEffect = effectFn
6 // 在调用副作用函数之前将当前副作用函数压栈
7 effectStack.push(effectFn)
8
9 // 将 fn 的执行结果存储到 res 中
10 // 新增
11 const res = fn()
12
13 // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,
14 // 并还原 activeEffect 为之前的值
15 effectStack.pop()
16 activeEffect = effectStack[effectStack.length - 1]
17
18 // 将 res 作为 effectFn 的返回值
19 // 新增
20 return res
21 }
22 // 将 options 挂在到 effectFn 上
23 effectFn.options = options
24 // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
25 effectFn.deps = []
26
27 // 执行副作用函数
28 // 新增
29 if (!options.lazy) {
30 effectFn()
31 }
32 return effectFn
33}
34
1function computed(getter) {
2 // value 用来缓存上一次计算的值
3 let value
4 // dirty 标志,用来标识是否需要重新计算值,
5 // 为 true 则意味着“脏”,需要计算
6 let dirty = true
7
8 // 把 getter 作为副作用函数,创建一个 lazy 的 effect
9 const effectFn = effect(getter, {
10 lazy: true,
11 scheduler() {
12 // 添加调度器,在调度器中将 dirty 重置为 true
13 if (!dirty) {
14 dirty = true
15 // 当计算属性依赖的响应式数据变化时,
16 // 手动调用 trigger 函数触发响应
17 trigger(obj, 'value')
18 }
19 }
20 })
21
22 const obj = {
23 // 当读取 value 时才执行 effectFn
24 get value() {
25 // 只有“脏”时才计算值,并将得到的值缓存到 value 中
26 if (dirty) {
27 value = effectFn()
28 // 将 dirty 设置为 false,
29 // 下一次访问直接使用缓存到 value 中的值
30 dirty = false
31 }
32 // 当读取 value 时,手动调用 track 函数进行追踪
33 track(obj, 'value')
34 return value
35 }
36 }
37
38 return obj
39}
40
watch 的实现原理
1function traverse(value, seen = new Set()) {
2 // 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
3 if (typeof value !== 'object' || value === null || seen.has(value)) return
4 // 将数据添加到 seen 中,代表遍历地读取过了,
5 // 避免循环引用引起的死循环
6 seen.add(value)
7
8 // 暂时不考虑数组等其他结构
9 // 假设 value 就是一个对象,
10 // 使用 for...in 读取对象的每一个值,
11 // 并递归地调用 traverse 进行处理
12 for (const k in value) {
13 traverse(value[k], seen)
14 }
15
16 return value
17}
18
19// watch 函数接收三个参数,
20// source 是响应式数据,cb 是回调函数
21function watch(source, cb, options = {}) {
22 let getter
23 // 如果 source 是函数,说明用户传递的是 getter,
24 // 所以直接把 source 赋值给 getter
25 if (typeof source === 'function') {
26 getter = source
27 } else {
28 // 调用 traverse 递归地读取
29 getter = () => traverse(source)
30 }
31
32 // 定义旧值与新值
33 let oldValue, newValue
34
35 // cleanup 用来存储用户注册的过期回调
36 let cleanup
37 // 定义 onInvalidate 函数
38 function onInvalidate(fn) {
39 // 将过期回调存储到 cleanup 中
40 cleanup = fn
41 }
42
43 // 提取 scheduler 调度函数为一个独立的 job 函数
44 const job = () => {
45 // 在 scheduler 中重新执行副作用函数,得到的是新值
46 newValue = effectFn()
47 // 在调用回调函数 cb 之前,先调用过期回调
48 if (cleanup) {
49 cleanup()
50 }
51 // 将旧值和新值作为回调函数的参数
52 // 将 onInvalidate 作为回调函数的第三个参数,以便用户使用
53 cb(oldValue, newValue, onInvalidate)
54 // 更新旧值,不然下一次会得到错误的旧值
55 oldValue = newValue
56 }
57
58 // 使用 effect 注册副作用函数时,开启 lazy 选项,
59 // 并把返回值存储到 effectFn 中以便后续手动调用
60 const effectFn = effect(
61 // 触发读取操作,从而建立联系
62 // 执行 getter
63 () => getter(),
64 {
65 lazy: true,
66 scheduler: () => {
67 // 在调度函数中判断 flush 是否为 'post',
68 // 如果是,将其放到微任务队列中执行
69 // 从而实现异步延迟执行
70 if (options.flush === 'post') {
71 const p = Promise.resolve()
72 p.then(job)
73 } else {
74 job()
75 }
76 }
77 }
78 )
79
80 // 当 immediate 为 true 时立即执行 job,从而触发回调执行
81 if (options.immediate) {
82 job()
83 } else {
84 // 手动调用副作用函数,拿到的值就是旧值
85 oldValue = effectFn()
86 }
87}
88
立即执行的 watch 与回调执行时机
上述代码 options.immediate
和 options.flush
相关内容
过期的副作用
上述代码 onInvalidate
相关内容
完整代码如下
1// 存储副作用函数的桶
2const bucket = new WeakMap()
3
4// 原始数据
5const data = { foo: 1, bar: 2 }
6// 对原始数据的代理
7const obj = new Proxy(data, {
8 // 拦截读取操作
9 get(target, key) {
10 // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
11 track(target, key)
12 // 返回属性值
13 return target[key]
14 },
15 // 拦截设置操作
16 set(target, key, newVal) {
17 // 设置属性值
18 target[key] = newVal
19 // 把副作用函数从桶里取出并执行
20 trigger(target, key)
21 }
22})
23
24function track(target, key) {
25 if (!activeEffect) return
26 let depsMap = bucket.get(target)
27 if (!depsMap) {
28 bucket.set(target, (depsMap = new Map()))
29 }
30 let deps = depsMap.get(key)
31 if (!deps) {
32 depsMap.set(key, (deps = new Set()))
33 }
34 deps.add(activeEffect)
35 activeEffect.deps.push(deps)
36}
37
38function trigger(target, key) {
39 const depsMap = bucket.get(target)
40 if (!depsMap) return
41 const effects = depsMap.get(key)
42
43 const effectsToRun = new Set()
44 effects && effects.forEach(effectFn => {
45 if (effectFn !== activeEffect) {
46 effectsToRun.add(effectFn)
47 }
48 })
49 effectsToRun.forEach(effectFn => {
50 if (effectFn.options.scheduler) {
51 effectFn.options.scheduler(effectFn)
52 } else {
53 effectFn()
54 }
55 })
56 // effects && effects.forEach(effectFn => effectFn())
57}
58
59// 用一个全局变量存储当前激活的 effect 函数
60let activeEffect
61// effect 栈
62const effectStack = []
63
64function effect(fn, options = {}) {
65 const effectFn = () => {
66 cleanup(effectFn)
67 // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
68 activeEffect = effectFn
69 // 在调用副作用函数之前将当前副作用函数压栈
70 effectStack.push(effectFn)
71 const res = fn()
72 // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
73 effectStack.pop()
74 activeEffect = effectStack[effectStack.length - 1]
75
76 return res
77 }
78 // 将 options 挂在到 effectFn 上
79 effectFn.options = options
80 // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
81 effectFn.deps = []
82 // 执行副作用函数
83 if (!options.lazy) {
84 effectFn()
85 }
86
87 return effectFn
88}
89
90function cleanup(effectFn) {
91 for (let i = 0; i < effectFn.deps.length; i++) {
92 const deps = effectFn.deps[i]
93 deps.delete(effectFn)
94 }
95 effectFn.deps.length = 0
96}
97
98// =========================
99
100function traverse(value, seen = new Set()) {
101 if (typeof value !== 'object' || value === null || seen.has(value)) return
102 seen.add(value)
103 for (const k in value) {
104 traverse(value[k], seen)
105 }
106
107 return value
108}
109
110function watch(source, cb, options = {}) {
111 let getter
112 if (typeof source === 'function') {
113 getter = source
114 } else {
115 getter = () => traverse(source)
116 }
117
118 let oldValue, newValue
119
120 let cleanup
121 function onInvalidate(fn) {
122 cleanup = fn
123 }
124
125 const job = () => {
126 newValue = effectFn()
127 if (cleanup) {
128 cleanup()
129 }
130 cb(oldValue, newValue, onInvalidate)
131 oldValue = newValue
132 }
133
134 const effectFn = effect(
135 // 执行 getter
136 () => getter(),
137 {
138 lazy: true,
139 scheduler: () => {
140 if (options.flush === 'post') {
141 const p = Promise.resolve()
142 p.then(job)
143 } else {
144 job()
145 }
146 }
147 }
148 )
149
150 if (options.immediate) {
151 job()
152 } else {
153 oldValue = effectFn()
154 }
155}
156
157let count = 0
158function fetch() {
159 count++
160 const res = count === 1 ? 'A' : 'B'
161 return new Promise(resolve => {
162 setTimeout(() => {
163 resolve(res)
164 }, count === 1 ? 1000 : 100);
165 })
166}
167
168let finallyData
169
170watch(() => obj.foo, async (newVal, oldVal, onInvalidate) => {
171 let valid = true
172 onInvalidate(() => {
173 valid = false
174 })
175 const res = await fetch()
176
177 if (!valid) return
178
179 finallyData = res
180 console.log(finallyData)
181})
182
183obj.foo++
184setTimeout(() => {
185 obj.foo++
186}, 200);
187
小结
这部分还是挺重要,而且确实也挺复杂的,需要好好盘点一下,建议都去看看源码,可以结合《Vue.js的设计与实现》这本书一起看。
确实能学到了很多设计思想,编程思想。感觉确实能在我们的实际代码中也能用到。
参考资料
《Vue.js的设计与实现》
相关笔记