Jansiel Notes

通过 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.immediateoptions.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的设计与实现》