响应式系统作用与实现 -- VUE.js设计与实现 (3)

110 阅读4分钟

第四章响应式系统作用与实现

相关代码可以在我的github库进行提取-> 响应式数据

响应式数据副作用

副作用:effect函数的执行会直接或间接的影响其它函数的执行。

响应式数据:在修改响应式数据时,副作用函数会感知变化并重新执行。

响应式数据的基本实现

思考:变成响应式数据有两点线索。

  • 数据设置触发effect函数
  • effect中对数据进行了读取

如此以来我们如果可以拦截数据的读取与写入,在写入时可以将读取数据的effect函数触发。就可以进行响应式数据。

读取时。将副作用effect函数存入副作用桶中。 写入时,将副作用桶中的函数取出并执行。

问题便是如何可以对对象进行拦截。

  • Vue2的实现方式:ES2015之前,只可以用Object.defineProperty函数实现。
  • Vue3的实现方式:使用代理对象Proxy

如下是一个简单的实现 image.png

其中如果收集effect函数就可以进行优化如下。

image.png

使用全局变量activeEffect保存副作用函数,然后使用effect函数进行一个注册。再次调用proxy时就可以直接使用activeEffect进行获取副作用函数。

但由于我们没对字段和effect进行强绑定,只要触发set就会触发effect。做出如下优化。

image.png

这里使用weakMap作为对象target的引用,因为其作用弱引用不会影响垃圾回收机制。用于储存只有当key存在时,才有价值的信息。

其中将副作用收集的部分抽离成track函数,如其名相同进行追踪副作用和依赖。把触发effect抽离成trigger函数中。

但还是无法对分支情况进行兼容,例如使用obj.ok ? obj.text : null 如果obj.ok从ture变成false。obj.text本应该成为不影响副作用函数的数据。但仍然存在于depsMap中。所以effect函数定义时保存所有依赖的响应式数据的deps,然后执行时进行清除。

image.png image.png

但其实此代码会因为反复清除与增加Map中的值导致trigger函数无法结束,而无限循环。如下。

image.png

避免方法就是使用另一个Set进行遍及它。new Set(args)

应对嵌套effect

image.png 使用栈实时指向顶层的effect函数,而不用担心activeEffect被直接覆盖消失。

避免无限循环,如果effect函数中存在类似 obj.i++ 这样包含取值和赋值就会在track进行时进行trigger函数,trigger函数对收集的副作用调用自身导致无限调用->栈溢出。增加如下代码到trigger函数中。

image.png 当触发函数为当前activeEffect不再执行。

为了基于用户更多的控制权限,让用户可以控制effectFn 的执行机制。创建 effect 时加入 option对象。包含 scheduler 调度器。在 trigger 触发 effectFn 时如果存在调度器将 effectFn 传入调度器中。基于用户控制。

image.png

除了控制副作用函数的执行顺序还需要控制其执行次数。

使用 set 做成任务队列即使加入多次 effect 函数也只是触发一次计算。

但其实我们为返回 effect 函数的结果。做出如下修改。将 fn 的返回值return。

image.png

这样我们就可以获得任何可以收集依赖的值进行计算。也就是 computed 函数。

image.png

其中在值变更时进行触发 trigger 函数为了让取得 computed 值的 obj.value获取副作用触发信息。

Watch 设计原理

watch 函数作为当值触发调用回调函数,其实就是利用了 effect 中的 option.scheduler 和 data。

改变传入值为 source,判断类型为 function 时代替封装好的遍及函数。

image.png 此外 watch 函数会将 oldValue 和 newValue 进行传入 callback 中。我们将内部定义的effectFn 定义为 lazy 函数,然后再 scheduler 中手动触发取得旧值。在set 后进而取得新值进行传入对比。

image.png

增加让副作用过期的功能,防止使用异步动作时,A 请求接着 B 请求,而 A 的请求 后于 B 的请求导致数据不匹配的问题。增加 onInvalidate 参数于 callback中。让 callback 执行时,知道运行时是否过期。

image.png