We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
在2023.12.28日刚发布的 Vue3.4 版本当中重构了部分响应式系统的功能。博客当中举了一个例子:
在之前的版本当中,count.value 发生变化的话,但是 isEven.value 不一定真正的发生了变化,但是仍然会再次触发 watchEffect 的执行。主要的原因还是在于之前的 computed effect 的设计,computed 依赖的响应式数据发生了变化之后,computed effect scheduler 会立即触发对其产生依赖的 effect。所以在这个例子当中,count.value 发生了变化,触发 computed effect 进而也就触发了 watch effect 的执行。
count.value
isEven.value
watchEffect
由这个简单的例子可以继续推导下,在 Vue 框架内部基于 ReactiveEffect 封装了更加上层的响应式 api 的使用场景,包括:
在不同的使用场景下,这些 effect 都可以和 computed 数据建立起依赖关系。
那么不管是以上哪种依赖关系, computed 数据在 re-computed 的过程当中都是可能会出现上述例子当中出现的:computed 数据的值实际没有变化,但是 effect 会重新执行的情况,从而导致了一些不必要的性能损耗。
那么为了优化这种场景,Vue3.4 引入了 effect dirty check 机制:
首先来看下 ReactiveEffect 重构后几个大的变化:
trigger
_dirtyLevel
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 } }
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 } ... }
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 依赖关系触发流程重构前后的工作流程:
在优化后的流程当中依据依赖关系触发 effect scheduler 的流程没太大变化,不过在触发 effect 的过程当中新增了对于 effect dirty 状态的更新,尤其是 computed 触发其依赖 effect 会将对应的 dirty 状态更新为 ComputedValueMaybeDirty,进入到 effect scheduler 调用的流程当中通过对 effect dirty check 来决定是否进行 scheduler 后续的流程(开发者需要手动调用),也就是 effect scheduler 后续的调用。
ComputedValueMaybeDirty
那么对于 effect dirty check 的流程来说,实际也就是看和当前 effect 有依赖关系的 computed 数据是否真的发生了变化(触发 computed value getter 的过程),一旦有一个 computed 数据发生了变化也就会更新 effect dirty 的状态为 ComputedValueDirty。
ComputedValueDirty
ReactiveEffect 是 @vue/reactivity 所暴露出最重要最底层的用以搭建整个响应式系统的 api,那么如果要基于 3.4 版本后的 ReactiveEffect 去封装上层的响应式 api 有两点需要注意:
@vue/reactivity
The text was updated successfully, but these errors were encountered:
到位。
我这么理解对不对 ,就是先 pauseTracking 跑一遍 getter 看数据变没变,没变的话后边的 effect 就不用跑了。
Sorry, something went wrong.
@jaskang 嗯你的理解是对的。dirty check 如果没有发生变化的话,后续 effect 的代码就不需要执行了。
const effect = new ReactiveEffect(fn, () => {}, () => { if (effect.dirty) { effect.run() } })
这个逻辑在响应式系统里还挺常见的,思路是在执行具体的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是否真的发生了变化,如果没有变化,是不需要重新计算值/执行副作用的。
@githubxiaowen 感谢分享
No branches or pull requests
框架现状
在2023.12.28日刚发布的 Vue3.4 版本当中重构了部分响应式系统的功能。博客当中举了一个例子:
在之前的版本当中,
count.value
发生变化的话,但是isEven.value
不一定真正的发生了变化,但是仍然会再次触发watchEffect
的执行。主要的原因还是在于之前的 computed effect 的设计,computed 依赖的响应式数据发生了变化之后,computed effect scheduler 会立即触发对其产生依赖的 effect。所以在这个例子当中,count.value 发生了变化,触发 computed effect 进而也就触发了 watch effect 的执行。由这个简单的例子可以继续推导下,在 Vue 框架内部基于 ReactiveEffect 封装了更加上层的响应式 api 的使用场景,包括:
在不同的使用场景下,这些 effect 都可以和 computed 数据建立起依赖关系。
那么不管是以上哪种依赖关系, computed 数据在 re-computed 的过程当中都是可能会出现上述例子当中出现的:computed 数据的值实际没有变化,但是 effect 会重新执行的情况,从而导致了一些不必要的性能损耗。
那么为了优化这种场景,Vue3.4 引入了 effect dirty check 机制:
ReactiveEffect 重构
首先来看下 ReactiveEffect 重构后几个大的变化:
trigger
函数(trigger 和 scheduler 之间的区别:trigger 要比 scheduler 先执行,提前派发一些信号,主要是用以 computed 数据的处理)_dirtyLevel
用以标记当前 effect 实例的 dirty 状态(区分了 computed 数据和普通的 reactive/ref 数据)computed 重构
triggerEffects 重构
再回到在 Blog 当中的例子,看下响应式数据发生变化后整个 effect 依赖关系触发流程重构前后的工作流程:
在优化后的流程当中依据依赖关系触发 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 有两点需要注意:The text was updated successfully, but these errors were encountered: