◆

Vue3 源码:computed 计算属性缓存实现原理

2023-04-19 00:390

computed 和 effect 的区别在哪

computed 和普通的 effect 都是「包一段函数,依赖变了就重新执行」,区别在于两件事:computed 有返回值,而且它是懒的。

普通 effect 创建时立刻执行一次,之后依赖一变就马上再跑。computed 不一样,声明的时候不算,只有真正去读 .value 才算;算完把结果存下来,后面反复读 .value 都直接给存好的值,不会重复执行 getter;只有依赖真的变了,才把缓存标记为失效,等下一次读取时再重算一遍。

这个「算过就存着、依赖变了才失效」的机制,实现上就是一个布尔开关 _dirty。理解 computed 的核心只需要盯住这个变量:它什么时候被置为 true,什么时候被置为 false,以及谁负责翻它。

依赖 ReactiveEffect 的调度器

computed 能做到懒计算,靠的是 ReactiveEffect 的第二个参数 scheduler(调度器)。ReactiveEffect 内部有个约定:依赖变化触发 trigger 时,如果这个 effect 传了 scheduler,就调用 scheduler 而不是直接调用 run 执行用户函数。

这一层间接调用给了 computed 插手的机会。依赖变了,它不急着重算,只在 scheduler 里把 _dirty 翻回 true(相当于把缓存作废),然后通知依赖自己的那些 effect。真正的计算推迟到下一次有人读 .value 时才发生。如果这期间没人读,这次计算就彻底省掉了。

代码执行流程

下面的实现只有几十行,按三步走:

  1. 在计算属性声明时,直接执行 ComputedRefImpl 中的 constructor,主要是执行了 effect 中的 ReactiveEffect,ReactiveEffect 中有一个调度函数,所以下面的代码相当于是执行调度函数将 _dirty 设置为 true triggerRefValue 就是为了执行这个调用函数
  2. 调用 .value 触发 get,获取到最新的计算属性值,所以当计算属性没有主动获取值时,是不会进行计算的
  3. this._dirty 这个值就是为了控制,只有在调用 .value 时才去计算值

看代码时重点关注 constructor 里传给 ReactiveEffect 的那个箭头函数(也就是 scheduler),以及 get value 里对 _dirty 的判断,这两处配合起来就是缓存的全部逻辑:

import { createDep } from './dep';
import { ReactiveEffect } from './effect';
import { trackRefValue, triggerRefValue } from './ref';

export class ComputedRefImpl {
  public dep: any;
  public effect: ReactiveEffect;

  private _dirty: boolean;
  private _value;

  constructor(getter) {
    this._dirty = true;
    this.dep = createDep();
    this.effect = new ReactiveEffect(getter, () => {
      // scheduler
      // 只要触发了这个函数说明响应式对象的值发生改变了
      // 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
      if (this._dirty) return;

      this._dirty = true;
      triggerRefValue(this);
    });
  }

  get value() {
    // 收集依赖
    trackRefValue(this);
    // 锁上,只可以调用一次
    // 当数据改变的时候才会解锁
    // 这里就是缓存实现的核心
    // 解锁是在 scheduler 里面做的
    if (this._dirty) {
      this._dirty = false;
      // 这里执行 run 的话,就是执行用户传入的 fn
      this._value = this.effect.run();
    }

    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}

几个容易忽略的细节

_dirty 初始值是 true。这样第一次读 .value 必定会走进计算分支,不需要在构造函数里预先跑一次 getter,懒计算从声明那一刻就成立了。

scheduler 里那句 if (this._dirty) return 是一道去重闸门。假设一个计算属性依赖了三个响应式值,一次操作里把三个都改了,scheduler 会被触发三次。第一次已经把 _dirty 置为 true 并通知过下游,后两次直接返回,避免重复通知造成下游 effect 多次执行。

get value 里 trackRefValue(this) 在 _dirty 判断之前,位置不能挪。因为不管这次是重新计算还是直接返回缓存,读取者都必须被收集进来,否则缓存命中的那些读取就漏掉了依赖关系,后面值变了它们收不到通知。

计算链路上还藏着一层依赖传递。ComputedRefImpl 自己带 dep,它既是依赖的收集者(收集读它的 effect),又是别人的依赖(它的 getter 里读了 reactive 对象,被那些对象收集)。所以 computed 可以嵌套 computed,_dirty 的失效会顺着这条链一级级传下去。

最后一点:既然计算结果会被缓存,getter 里就不该写副作用,比如改别的响应式值、发请求、操作 DOM。这些动作的执行次数会随缓存命中情况变化,行为不可预期。需要副作用的场景应该用 watch 或 watchEffect。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue3 源码:computed 计算属性缓存实现原理加载中