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 = false
obj.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> {
  // 用于存储 dep
  deps: Dep[] = []
}
export let activeEffect: ReactiveEffect | undefined

export 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 } = effect
  if (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执行了
Vue
Hello
 1s 后 
副作用函数1执行了
副作用函数2执行了
Vue
你好

但是,实际的输出是:

副作用函数1执行了
副作用函数2执行了
Vue
Hello
 1s 后 
副作用函数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 | undefined

export class ReactiveEffect<T = any> {
  parent: ReactiveEffect | undefined = undefined
  
  run() {
    try {
      this.parent = activeEffect
      activeEffect = this

      cleanupEffect(this)
      return this.fn()
    } finally {
      activeEffect = this.parent
      this.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 的实现原理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答你的问题。首先,我们需要了解Vue3的响应系统是如何工作的。Vue3使用了一个名为`Reactive`的函数来实现响应。 `Reactive`函数的作用是将一个普通的JavaScript对象转换成响应的对象。当响应对象的属性被修改时,所有依赖该属性的地方都会自动更新。 下面是`Reactive`函数的实现: ```javascript function Reactive(obj) { const handlers = { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); track(target, prop); return isObject(value) ? Reactive(value) : value; }, set(target, prop, value, receiver) { const oldValue = Reflect.get(target, prop, receiver); let result = true; if (oldValue !== value) { result = Reflect.set(target, prop, value, receiver); trigger(target, prop); } return result; }, deleteProperty(target, prop) { const result = Reflect.deleteProperty(target, prop); trigger(target, prop); return result; } }; return new Proxy(obj, handlers); } ``` `Reactive`函数接受一个普通的JavaScript对象作为参数,返回一个响应的对象。在实现中,我们使用了ES6的Proxy对象来实现响应。 在`get`处理器中,我们使用了`track`函数来收集依赖。`track`函数的作用是将当前正在执行的计算函数添加到依赖列表中。 在`set`处理器中,我们首先获取旧值,然后判断新值是否与旧值相同。如果不同,我们使用`trigger`函数来触发更新。`trigger`函数的作用是遍历依赖列表,执行所有计算函数。 在`deleteProperty`处理器中,我们使用`trigger`函数来触发更新,因为删除属性也可能导致依赖更新。 在以上代码中,我们还使用了`isObject`函数来判断一个值是否为对象。该函数的实现如下: ```javascript function isObject(value) { return typeof value === 'object' && value !== null; } ``` 这个函数非常简单,它只是判断一个值是否为对象。如果是对象,我们就递归调用`Reactive`函数来将该对象转换成响应。 总之,这就是Vue3的响应系统的实现原理。通过`Reactive`函数和Proxy对象,我们可以将一个普通的JavaScript对象转换成响应的对象,并实现自动更新。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值