Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue3.4 effect-drity-check 机制 #60

Open
CommanderXL opened this issue Jan 16, 2024 · 4 comments
Open

Vue3.4 effect-drity-check 机制 #60

CommanderXL opened this issue Jan 16, 2024 · 4 comments
Labels
Vue.js Extra attention is needed

Comments

@CommanderXL
Copy link
Owner

框架现状

在2023.12.28日刚发布的 Vue3.4 版本当中重构了部分响应式系统的功能。博客当中举了一个例子:

vue3 4-blog

在之前的版本当中,count.value 发生变化的话,但是 isEven.value 不一定真正的发生了变化,但是仍然会再次触发 watchEffect 的执行。主要的原因还是在于之前的 computed effect 的设计,computed 依赖的响应式数据发生了变化之后,computed effect scheduler 会立即触发对其产生依赖的 effect。所以在这个例子当中,count.value 发生了变化,触发 computed effect 进而也就触发了 watch effect 的执行。

由这个简单的例子可以继续推导下,在 Vue 框架内部基于 ReactiveEffect 封装了更加上层的响应式 api 的使用场景,包括:

  • computed effect
  • watch effect
  • effect api
  • render effect(组件渲染使用)

在不同的使用场景下,这些 effect 都可以和 computed 数据建立起依赖关系。

computed-value-effects

那么不管是以上哪种依赖关系, computed 数据在 re-computed 的过程当中都是可能会出现上述例子当中出现的:computed 数据的值实际没有变化,但是 effect 会重新执行的情况,从而导致了一些不必要的性能损耗。

那么为了优化这种场景,Vue3.4 引入了 effect dirty check 机制:

ReactiveEffect 重构

首先来看下 ReactiveEffect 重构后几个大的变化:

  • 对于使用方而言第二个参数改为必传的 trigger 函数(trigger 和 scheduler 之间的区别:trigger 要比 scheduler 先执行,提前派发一些信号,主要是用以 computed 数据的处理)
  • 内部新增了一个属性 _dirtyLevel 用以标记当前 effect 实例的 dirty 状态(区分了 computed 数据和普通的 reactive/ref 数据)
  • 暴露了 dirty getter 用以判断当前 effect scheduler 是否真的需要重新触发
export class ReactiveEffect {
  ...
  _dirtyLevel = DirtyLevels.Dirty
  ...

  constructor(
    public fn: () => T,
    public trigger: () => void,
    public scheduler?: EffectScheduler,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }

  public get dirty() {
    if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) {
      this._dirtyLevel = DirtyLevels.NotDirty // computed 数据访问过一次后,置为 NotDirty
      this._queryings++ // 针对查询 computed 数据的设置(不涉及依赖关系的建立)
      pauseTracking() // 暂停依赖的收集
      for (const dep of this.deps) {
        if (dep.computed) {
          triggerComputed(dep.computed) // 访问存在依赖关系的 computed 数据,调用 computed value getter,看是否发生了变化,如果发生了变化,动态的改变当前依赖的 effect dirty 值,进而最终会执行 effect scheduler 
          if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) {
            break
          }
        }
      }
      resetTracking()
      this._queryings--
    }
    return this._dirtyLevel >= DirtyLevels.ComputedValueDirty
  }
}

computed 重构

  • trigger event,更新 effect dirty 为 ComputedValueMayBeDirty(仍然保持了 computed 的 lazy 特性)
  • value 访问会判断 effect dirty 的状态,为 true 才会去重新执行 effect 并得到最新的数据,同时会对比新旧数据,如果发生了变化,那么就会 triggerEffects,实际上就是触发对其依赖的 effect
export class ComputedRefImpl<T> {
  ...
  constructor() {
    this.effect = new ReactiveEffect(
      () => getter(this._value),
      () => triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
    )
  }
  get value() {
    const self = toRaw(this)
    trackRefValue(self)
    if (!self._cacheable || self.effect.dirty) {
      if (hasChanged(self._value, (self._value = self.effect.run()!))) {
        triggerRefValue(self, DirtyLevels.ComputedValueDirty)
      }
    }
    return self._value
  }
  ...
}

triggerEffects 重构

  • 更新 effect dirty 状态
  • 触发 trigger
  • 收集 effect scheduler(若有),在适当的时机会触发所有收集的 effect scheduler(一个数据发生变化后,确保整个数据依赖链路(包括effect )都被遍历过)
export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels
  ...
) {
  pauseScheduling() // 只能保证在当前 triggerEffects 的嵌套 triggerEffects 当中不会触发 effect scheduler 函数
  for (const effect of dep.keys()) {
    if (!effect.allowRecurse && effect._runnings) {
      continue
    }
    if (
      effect._dirtyLevel < dirtyLevel &&
      (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty) // runnings 当前 effect 是否正在执行
    ) {
      const lastDirtyLevel = effect._dirtyLevel
      effect._dirtyLevel = dirtyLevel
      if (
        lastDirtyLevel === DirtyLevels.NotDirty &&
        (!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
      ) {
        if (__DEV__) {
          effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
        }
        effect.trigger() // 确保 computed trigger 先执行
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler)
        }
      }
    }
  }
  resetScheduling()
}

再回到在 Blog 当中的例子,看下响应式数据发生变化后整个 effect 依赖关系触发流程重构前后的工作流程:

refactor

在优化后的流程当中依据依赖关系触发 effect scheduler 的流程没太大变化,不过在触发 effect 的过程当中新增了对于 effect dirty 状态的更新,尤其是 computed 触发其依赖 effect 会将对应的 dirty 状态更新为 ComputedValueMaybeDirty,进入到 effect scheduler 调用的流程当中通过对 effect dirty check 来决定是否进行 scheduler 后续的流程(开发者需要手动调用),也就是 effect scheduler 后续的调用。

那么对于 effect dirty check 的流程来说,实际也就是看和当前 effect 有依赖关系的 computed 数据是否真的发生了变化(触发 computed value getter 的过程),一旦有一个 computed 数据发生了变化也就会更新 effect dirty 的状态为 ComputedValueDirty

基于 ReactiveEffect 的上层封装

ReactiveEffect 是 @vue/reactivity 所暴露出最重要最底层的用以搭建整个响应式系统的 api,那么如果要基于 3.4 版本后的 ReactiveEffect 去封装上层的响应式 api 有两点需要注意:

  1. ReactiveEffect 接受的 trigger 函数为必传;
  2. 可以在 effect scheduler 函数当中可以进行 effect dirty check 来决定 effect 是否需要执行用以提升性能;
@CommanderXL CommanderXL added the Vue.js Extra attention is needed label Jan 16, 2024
@jaskang
Copy link

jaskang commented Jan 17, 2024

到位。

我这么理解对不对 ,就是先 pauseTracking 跑一遍 getter 看数据变没变,没变的话后边的 effect 就不用跑了。

@CommanderXL
Copy link
Owner Author

@jaskang 嗯你的理解是对的。dirty check 如果没有发生变化的话,后续 effect 的代码就不需要执行了。

const effect = new ReactiveEffect(fn, () => {}, () => {
   if (effect.dirty) {
      effect.run()
   }
})

@githubxiaowen
Copy link

githubxiaowen commented Mar 14, 2024

这个逻辑在响应式系统里还挺常见的,思路是在执行具体的effect时前置判断一下依赖是否真的发生了变化

angular的signal和mobx是通过维护值版本来搞的,推荐下面两个文档,对响应式系统的常见问题和解法做了详细介绍。

https://github.com/angular/angular/blob/main/packages/core/primitives/signals/README.md#equality-semantics

https://en.wikipedia.org/wiki/Reactive_programming

对于一个表格里的公式单元格,其实也应用到类似思路,A -> B -> C。当C的内容发生变化时,A和B被标脏,在计算A时,可以前置判断B是否真的发生了变化,如果没有变化,是不需要重新计算值/执行副作用的。

@CommanderXL
Copy link
Owner Author

@githubxiaowen 感谢分享

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Vue.js Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants