第一句子网 - 唯美句子、句子迷、好句子大全
第一句子网 > Vue3 源码阅读(3):响应式系统 —— 重置 effect 的依赖收集 嵌套的 effect effect 调度执行

Vue3 源码阅读(3):响应式系统 —— 重置 effect 的依赖收集 嵌套的 effect effect 调度执行

时间:2018-08-21 14:35:03

相关推荐

Vue3 源码阅读(3):响应式系统 —— 重置 effect 的依赖收集 嵌套的 effect effect 调度执行

上一篇博客讲解了 Vue3 响应式系统的核心思想,但真正的响应式系统不可能这么简单,真实的业务场景中,会有很多更加复杂的场景,这一篇博客,我们来增强上一篇中实现的响应式系统。

1,重置 effect 的依赖收集

首先说说为什么要重置 effect 的依赖收集,看下面的代码:

let obj = reactive({status: true,text1: 'hello',text2: 'Vue'})effect(() => {document.body.innerText = obj.status? obj.text1: obj.text2})obj.status = falseobj.text1 = 'new hello'

首次执行副作用函数时,副作用函数读取了 status 和 text1 属性,此时 status 和 text1 属性对应的 dep 应该收集了当前的这个 ReactiveEffect 实例,然后我们将 obj.status 改成了 false,这会导致副作用函数重新执行,重新执行的副作用函数读取了 status 和 text2 属性,在页面中渲染了 “Vue”。接下来,我们变更了 obj.text1,在理想的情况下,变更 obj.text1 不应该触发副作用函数的重新执行,因为上一次副作用函数的执行并没有读取 text1 属性,但是,当我们变更text1 属性后,发现副作用函数重新执行了。副作用函数重新执行的原因是 text1 属性对应的 dep(Set 实例)依旧保存着当前的ReactiveEffect 实例。所以,我们需要做的事情就是在副作用函数重新执行前,重置当前 ReactiveEffect 实例的依赖收集。

要想重置ReactiveEffect 实例的依赖收集,我们需要知道有哪些 dep 收集了当前的这个 ReactiveEffect 实例,一个副作用函数的执行有可能读取了多个响应式数据,所以一个ReactiveEffect 实例对应多个 dep,我们现在首先要做的事情是:在依赖收集的过程中,将收集了当前 ReactiveEffect 实例的 dep 缓存到ReactiveEffect 实例中,简要代码如下所示:

export class ReactiveEffect<T = any> {// 用于存储 depdeps: Dep[] = []}

export let activeEffect: ReactiveEffect | undefinedexport function trackEffects(dep: Dep) {dep.add(activeEffect!)activeEffect!.deps.push(dep)}

接下来,需要做的事情是在副作用函数重新执行前,重置 effect 的依赖收集,简要代码如下所示:

class ReactiveEffect {constructor(public fn) {}run(){activeEffect = this// 进行重置操作cleanupEffect(this)this.fn()activeEffect = undefined}}// 清空指定 ReactiveEffect 实例的依赖收集function cleanupEffect(effect: ReactiveEffect) {const { deps } = effectif (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].delete(effect)}deps.length = 0}}

2,嵌套的 effect

effect 函数是允许嵌套使用的,代码如下所示:

let obj = reactive({text1: 'Hello',text2: 'Vue'})effect(() => {console.log("副作用函数1执行了")effect(() => {console.log("副作用函数2执行了")console.log(obj.text2)})console.log(obj.text1)})setTimeout(() => {obj.text1 = '你好'}, 1000)

在副作用函数1中,我们又执行了一个 effect(() => {}),这创建了 effect2,并且,我们在 effect1 中读取了 text1 属性,在 effect2 中读取了 text2 属性,在理想的情况下,text1 属性对应的 dep 会收集 effect1,text2 属性对应的 dep 会收集 effect2,当我们变更 text1 属性时,正确的输出应该如下所示:

副作用函数1执行了副作用函数2执行了VueHello1s 后 副作用函数1执行了副作用函数2执行了Vue你好

但是,实际的输出是:

副作用函数1执行了副作用函数2执行了VueHello1s 后 副作用函数2执行了Vue

导致这个现象的原因是:当前的依赖收集有问题,我们回顾一下上一篇博客的内容,在 ReactiveEffect 的 run 方法中,我们会将当前的 ReactiveEffect 实例赋值到全局的activeEffect 变量上,然后执行副作用函数,副作用函数执行触发响应式数据的 get,在 get 中获取全局的activeEffect,然后进行依赖的收集。

问题就出现在将当前的 ReactiveEffect 实例赋值到全局的activeEffect 变量上,当我们执行嵌套的 effect 时,ReactiveEffect2 会被赋值到activeEffect 变量上,这会覆盖ReactiveEffect1,后续代码会读取 text1 属性,这会触发 text1 的 get,进而会导致 text1 属性对应的 dep 收集了ReactiveEffect2,所以当我们变更 text1 属性的时候,会重新执行副作用函数2。

知道了问题的原因,解决起来就容易了,Vue的做法是在ReactiveEffect 的实例上维护一个parent 属性,当 ReactiveEffec2 的 run 函数时,先将当前的activeEffect(ReactiveEffec1) 维护到 this.parent 上,然后将自身实例赋值(ReactiveEffec2)到activeEffect 变量上,这样当副作用函数2执行的时候,副作用函数2中读取的响应式数据就会依赖收集ReactiveEffec2,当副作用函数2执行完成后,将this.parent(ReactiveEffec1) 恢复到activeEffect 变量上,这样当副作用函数1继续执行读取响应式数据,响应式数据对应的 dep 就会依赖收集ReactiveEffec1 了。简要代码如下所示:

export let activeEffect: ReactiveEffect | undefinedexport class ReactiveEffect<T = any> {parent: ReactiveEffect | undefined = undefinedrun() {try {this.parent = activeEffectactiveEffect = thiscleanupEffect(this)return this.fn()} finally {activeEffect = this.parentthis.parent = undefined}}}

3,effect 调度执行

在这里,我们回顾一下上一篇博客中副作用函数的重新执行机制,当我们变更响应式数据的时候,会触发响应式数据的 set,在 set 函数中,我们会执行trigger 函数,这个函数会进而触发对应 dep 中所有 ReactiveEffect 实例的 run 函数,run 函数会重新执行副作用函数。

在这里,副作用函数的重新执行是固定好的,不够灵活,无法做进一步的功能增强,接下来的很多功能点都依托于effect 的调度执行,例如:computed、watch 和 组件的异步渲染。

首先给出我们最终想要的效果,然后再给出源码。

let obj = reactive({text1: 'Hello'})effect(() => {console.log("副作用函数1执行了")console.log(obj.text1)}, {scheduler: () => {// 这个调度函数会在 text1 属性发生变更时触发执行。console.log('scheduler 函数执行了')}})setTimeout(() => {obj.text1 = '你好' // scheduler 函数执行了})

在这里,effect 函数有两个参数,第一个参数是副作用函数,第二个参数是一个配置对象,在配置对象中我们可以配置scheduler 函数,这个函数会在依赖的响应式数据发生变化时触发执行。

最简实现如下所示:

class ReactiveEffect {constructor(public fn, public scheduler) {}run(){}}function effect(fn, options?: ReactiveEffectOptions) {// _effect 就是依赖const _effect = new ReactiveEffect(fn, options.scheduler)_effect.run()}function triggerEffect(effect: ReactiveEffect) {if (effect.scheduler) {effect.scheduler()} else {effect.run()}}

effect 函数现在能够接受两个参数,第二个参数是一个对象,这个对象中有一个scheduler 函数,在 new ReactiveEffect的时候,将这个scheduler 函数作为第二个参数,scheduler 函数被保存到了ReactiveEffect 实例上,最关键的点在triggerEffect 函数中,在之前的实现中,triggerEffect 函数会执行 effect.run 函数,而在新版本中,triggerEffect 函数首先检查 effect 实例上有没有scheduler 函数,有的话,就执行scheduler 函数。至此,功能完成。

有了调度能力,我们就可以基于ReactiveEffect 实现更多强大的功能,这些功能点会在后面的博客中进行讲解。

4,总结

这一篇博客对响应式系统中的功能进行了增强,下一篇博客讲解 Vue3 中的 computed 和 watch 的实现原理。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。