Vue3.5 响应式原理笔记

与 vue 相关的笔记,导师说方法比编码更加重要,那就从分析vuejs/core开始吧。

  • 计划:分析 reactivity 包的内容。

  • github 上有个关于 vue3 响应式原理的电子书,不过最后 commit 时间是 4 年前,现在 vue3 代码组织已经与其不太一样了(都重构几轮了)。而且内容和组件渲染那块耦合在一起了感觉。网上有个关于 vue3.5 响应式原理重构的文章,写的很好,本文对此有参考,但是那个文章在 track 那块讲的感觉和源码还是不太一致。

Reactivity 实现:

入口:

export function reactive<T extends object>(target: T): Reactive<T>;
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target;
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}

注意这里的 isReadonly 的实现是如下:

/**
 * Checks whether the passed value is a readonly object. The properties of a
 * readonly object can change, but they can't be assigned directly via the
 * passed object.
 *
 * The proxies created by {@link readonly} and {@link shallowReadonly} are
 * both considered readonly, as is a computed ref without a set function.
 *
 * @param value - The value to check.
 * @see {@link https://vuejs.org/api/reactivity-utilities.html#isreadonly}
 */
export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY]);
}

这里是判断由readonly&shallowreadonly方法创建的 proxy 或是未定义 set 属性的 ref。不是如 Object.freeze 等类似方法冻结的对象。

对于createReactiveObject分析:

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      warn(
        `value cannot be made ${isReadonly ? "readonly" : "reactive"}: ${String(
          target
        )}`
      );
    }
    return target;
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    // 考虑readonly(reactive(object))
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target;
  }
  // only specific value types can be observed.
  const targetType = getTargetType(target);
  if (targetType === TargetType.INVALID) {
    return target;
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  return proxy;
}
  • 非对象和已代理对象不进行处理,除非即将对于一个已代理对象创建只读代理(前面提到的 readonly 方法)

getTargetType 实现如下:

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value));
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case "Object":
    case "Array":
      return TargetType.COMMON;
    case "Map":
    case "Set":
    case "WeakMap":
    case "WeakSet":
      return TargetType.COLLECTION;
    default:
      return TargetType.INVALID;
  }
}

通过这里我们可以发现,如果是被Object.preventExtensions处理的对象也是不合法的,无法正确跟踪,会返回原对象。具体原因与fix(reactive): use isExtensible instead of isFrozen #1753这个相关,和 seal 处理过的对象不合法类似,vue 需要对对象赋值一些额外的属性(参见那些 enumeration,理论上是不是用 weakmap 可以优化这一问题?)。以下是测试:

import { reactive } from "@vue/reactivity";

const uExObj = Object.preventExtensions({
  name: "John",
});

const uExReactive = reactive(uExObj);

uExReactive.name = "Doe";

console.log(uExReactive.name); // "Doe"

console.log(uExReactive === uExObj); // true

继续,这里 toRawType 和 Object.prototype.toString 类似,截取其中[8, -1]字符串内容。

下面分析 baseHandler

class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    protected readonly _isReadonly = false,
    protected readonly _isShallow = false
  ) {}

  get(target: Target, key: string | symbol, receiver: object): any {
    if (key === ReactiveFlags.SKIP) return target[ReactiveFlags.SKIP];

    const isReadonly = this._isReadonly,
      isShallow = this._isShallow;
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return isShallow;
    } else if (key === ReactiveFlags.RAW) {
      if (
        // 此处readonlyMap and reactiveMap都是reactivity.ts中的内容,target => proxy.用处就是toRaw函数
        receiver ===
          (isReadonly
            ? isShallow
              ? shallowReadonlyMap
              : readonlyMap
            : isShallow
            ? shallowReactiveMap
            : reactiveMap
          ).get(target) ||
        // receiver is not the reactive proxy, but has the same prototype
        // this means the receiver is a user proxy of the reactive proxy
        Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
      ) {
        return target;
      }
      // early return undefined
      return;
    }

    const targetIsArray = isArray(target);

    if (!isReadonly) {
      let fn: Function | undefined;
      // 这里是对于数组的一些操作进行自定义的实现。比如push就noTracking之类的。
      if (targetIsArray && (fn = arrayInstrumentations[key])) {
        return fn;
      }
      if (key === "hasOwnProperty") {
        return hasOwnProperty;
      }
    }

    const res = Reflect.get(
      target,
      key,
      // if this is a proxy wrapping a ref, return methods using the raw ref
      // as receiver so that we don't have to call `toRaw` on the ref in all
      // its class methods
      isRef(target) ? target : receiver
    );

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res;
    }

    // 执行依赖追踪
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key);
    }

    if (isShallow) {
      return res;
    }

    // 要是用js很难不出错😂。
    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      // 这里的isIntergerKey是无符号的。
      return targetIsArray && isIntegerKey(key) ? res : res.value;
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res);
    }

    return res;
  }
}

这里可以发现,如reactive({test: ref(1)}).test是可以直接读到值的,当然如上图,shallowRef 就没法自动解包。至于为什么要这样设计个人不是很明白,似乎是有些过度设计了吧,和 shallowRef 一样的返回应该更符合直觉吧,不过大佬的项目肯定有大佬的道理。这个特性在 vue3 的文档里也有提及,不过关于 shallowRef 不能自动包的事情并没有被提及。

值得注意的还有这一段:

if (isObject(res)) {
  // Convert returned value into a proxy as well. we do the isObject check
  // here to avoid invalid value warning. Also need to lazy access readonly
  // and reactive here to avoid circular dependency.
  return isReadonly ? readonly(res) : reactive(res);
}

Vue 的深层监听是在 get 中实现的,也就是说未涉及到的对象并不会被深层地转换为响应式对象,这个设计很巧妙。而且配合前面我们看到的 proxyMap,也实现了对于同一对象的响应式对象不会被重复创建。

接下来是 MutableReactiveHandler,对于 Object 和 Array 的实现。

class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(isShallow = false) {
    super(false, isShallow);
  }

  set(
    target: Record<string | symbol, unknown>,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = target[key];
    if (!this._isShallow) {
      const isOldValueReadonly = isReadonly(oldValue);
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue);
        value = toRaw(value);
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        if (isOldValueReadonly) {
          return false;
        } else {
          oldValue.value = value;
          return true;
        }
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
    const result = Reflect.set(
      target,
      key,
      value,
      isRef(target) ? target : receiver
    );
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value);
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue);
      }
    }
    return result;
  }

  deleteProperty(
    target: Record<string | symbol, unknown>,
    key: string | symbol
  ): boolean {
    const hadKey = hasOwn(target, key);
    const oldValue = target[key];
    const result = Reflect.deleteProperty(target, key);
    if (result && hadKey) {
      trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
    }
    return result;
  }

  has(target: Record<string | symbol, unknown>, key: string | symbol): boolean {
    const result = Reflect.has(target, key);
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
      track(target, TrackOpTypes.HAS, key);
    }
    return result;
  }

  ownKeys(target: Record<string | symbol, unknown>): (string | symbol)[] {
    track(
      target,
      TrackOpTypes.ITERATE,
      isArray(target) ? "length" : ITERATE_KEY
    );
    return Reflect.ownKeys(target);
  }
}

这里先关注 setHandler:

let oldValue = target[key];
if (!this._isShallow) {
  const isOldValueReadonly = isReadonly(oldValue);
  if (!isShallow(value) && !isReadonly(value)) {
    oldValue = toRaw(oldValue);
    value = toRaw(value);
  }
  if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
    if (isOldValueReadonly) {
      return false;
    } else {
      oldValue.value = value;
      return true;
    }
  }
} else {
  // in shallow mode, objects are set as-is regardless of reactive or not
}

这一部分与之前的部分结合,就会诞生以下效果:

const testRef = ref(1);
const testReactive = reactive({
  ref: testRef,
});
const testShallowReactive = shallowReactive({
  ref: testRef,
});
testReactive.ref = 2;
console.log(testReactive.ref, testRef.value); // 2, 2
testShallowReactive.ref.value = 3;
console.log(testShallowReactive.ref.value, testRef.value); // 3, 3

好在正常情况下 reactive 中嵌套 ref 的情况并不多见,并且 ts 也有良好的类型提醒。

然后是 ref 相关,实现相对不会复杂,这里贴出关键代码:

/**
 * @internal
 */
class RefImpl<T = any> {
  _value: T;
  private _rawValue: T;

  dep: Dep = new Dep();

  public readonly [ReactiveFlags.IS_REF] = true;
  public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false;

  constructor(value: T, isShallow: boolean) {
    this._rawValue = isShallow ? value : toRaw(value);
    this._value = isShallow ? value : toReactive(value);
    this[ReactiveFlags.IS_SHALLOW] = isShallow;
  }

  get value() {
    if (__DEV__) {
      this.dep.track({
        target: this,
        type: TrackOpTypes.GET,
        key: "value",
      });
    } else {
      this.dep.track();
    }
    return this._value;
  }

  set value(newValue) {
    const oldValue = this._rawValue;
    const useDirectValue =
      this[ReactiveFlags.IS_SHALLOW] ||
      isShallow(newValue) ||
      isReadonly(newValue);
    newValue = useDirectValue ? newValue : toRaw(newValue);
    if (hasChanged(newValue, oldValue)) {
      this._rawValue = newValue;
      this._value = useDirectValue ? newValue : toReactive(newValue);
      if (__DEV__) {
        this.dep.trigger({
          target: this,
          type: TriggerOpTypes.SET,
          key: "value",
          newValue,
          oldValue,
        });
      } else {
        this.dep.trigger();
      }
    }
  }
}

到这里,其实对于 vue3 的依赖收集的过程就比较明了了。接着看 track 和 trigger 的实现,也就是依赖收集的具体过程。 vue3.5 对响应式相关内容见进行了重构,这里以最新的 3.5.17 版本为基准。

vue3.5 的依赖收集过程的实现是通过一个二维双向链表实现的。同时还有一个版本计数用于控制依赖的订阅是否被触发。

以下涉及的三个基本对象的意义: Dep: 依赖,如 reactive 对象这样的概念,基本属性如下:

  version = 0
  /**
   * Link between this dep and the current active effect
   */
  activeLink?: Link = undefined

  /**
   * Doubly linked list representing the subscribing effects (tail)
   */
  subs?: Link = undefined

  /**
   * Doubly linked list representing the subscribing effects (head)
   * DEV only, for invoking onTrigger hooks in correct order
   */
  subsHead?: Link

  /**
   * For object property deps cleanup
   */
  map?: KeyToDepMap = undefined
  key?: unknown = undefined

  /**
   * Subscriber counter
   */
  sc: number = 0

Sub: 订阅,如 watch,类型是一个接口,具体如下:

/**
 * Subscriber is a type that tracks (or subscribes to) a list of deps.
 */
export interface Subscriber extends DebuggerOptions {
  /**
   * Head of the doubly linked list representing the deps
   * @internal
   */
  deps?: Link;
  /**
   * Tail of the same list
   * @internal
   */
  depsTail?: Link;
  /**
   * @internal
   */
  flags: EffectFlags;
  /**
   * @internal
   */
  next?: Subscriber;
  /**
   * returning `true` indicates it's a computed that needs to call notify
   * on its dep too
   * @internal
   */
  notify(): true | void;
}

需要注意的是这里的 deps 和 depsLinks 的属性的类型是 Link 不是 Dep Link:这里指双向链表的一个结点。实现如下:

export class Link {
  /**
   * - Before each effect run, all previous dep links' version are reset to -1
   * - During the run, a link's version is synced with the source dep on access
   * - After the run, links with version -1 (that were never used) are cleaned
   *   up
   */
  version: number;

  /**
   * Pointers for doubly-linked lists
   */
  nextDep?: Link;
  prevDep?: Link;
  nextSub?: Link;
  prevSub?: Link;
  prevActiveLink?: Link;

  constructor(public sub: Subscriber, public dep: Dep) {
    this.version = dep.version;
    this.nextDep =
      this.prevDep =
      this.nextSub =
      this.prevSub =
      this.prevActiveLink =
        undefined;
  }
}

注意 computed 同时属于 Dep 和 Sub,后面会涉及与其相关的特殊优化。

这里先分析 Dep 对象的 track 方法的实现:

track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
  if (!activeSub || !shouldTrack || activeSub === this.computed) {
    return
  }

  let link = this.activeLink
  if (link === undefined || link.sub !== activeSub) {
    link = this.activeLink = new Link(activeSub, this)

    // add the link to the activeEffect as a dep (as tail)
    if (!activeSub.deps) {
      activeSub.deps = activeSub.depsTail = link
    } else {
      link.prevDep = activeSub.depsTail
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link
    }

    addSub(link)
  } else if (link.version === -1) {
    // reused from last run - already a sub, just sync version
    link.version = this.version

    // If this dep has a next, it means it's not at the tail - move it to the
    // tail. This ensures the effect's dep list is in the order they are
    // accessed during evaluation.
    if (link.nextDep) {
      const next = link.nextDep
      next.prevDep = link.prevDep
      if (link.prevDep) {
        link.prevDep.nextDep = next
      }

      link.prevDep = activeSub.depsTail
      link.nextDep = undefined
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link

      // this was the head - point to the new head
      if (activeSub.deps === link) {
        activeSub.deps = next
      }
    }
  }

  if (__DEV__ && activeSub.onTrack) {
    activeSub.onTrack(
      extend(
        {
          effect: activeSub,
        },
        debugInfo,
      ),
    )
  }

  return link
}
// 涉及到的函数
function addSub(link: Link) {
  link.dep.sc++
  if (link.sub.flags & EffectFlags.TRACKING) {
    const computed = link.dep.computed
    // computed getting its first subscriber
    // enable tracking + lazily subscribe to all its deps
    if (computed && !link.dep.subs) {
      computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
      for (let l = computed.deps; l; l = l.nextDep) {
        addSub(l)
      }
    }

    const currentTail = link.dep.subs
    if (currentTail !== link) {
      link.prevSub = currentTail
      if (currentTail) currentTail.nextSub = link
    }

    if (__DEV__ && link.dep.subsHead === undefined) {
      link.dep.subsHead = link
    }

    link.dep.subs = link
  }
}

这里面 activeSub 是一个全局变量,表示当前正在处理的 Subscriber。

先判断 this.activeLink(Link between this dep and the current active effect),执行以下两种逻辑:

  1. 若 undefined(尚未建立链接)或是 this.activeLink.sub !== activeSub(链接不数期望的),则创建新的 Link 结点,之后将新结点链接到 activeSub.deps 末尾。然后执行 addSub 函数。

    对于 addSub 函数,执行时先将其指向的 dep 的 sc++(Subscriber counter),然后若 link.sub.flags 包含 EffectFlags.TRACKING 则执行后续依赖追踪逻辑:若其有 computed 属性,就执行特殊优化,否则将当前结点链接至当前 dep 的链表的尾部。对于计算属性的特殊优化内容如下:

    先判断是否有 link.dep.subs 若空则跳过 computed 的特殊处理,以下是我的理解:这里的设计实际上是在第一次执行 addSub 的时候不进行 if 内的逻辑,因为后面的逻辑就会将 link.dep.subs 进行赋值(上文的链接到末尾部分)。if 块内的内容逻辑是将 computed 标记为 EffectFlags.TRACKING | EffectFlags.DIRTY,其中 DIRTY 的含义是需要重新进行计算。然后从 computed 的 deps 结尾向前依次初始化。

    总结一下就是当 dep 和 sub 的链接未创建时创建一个结点进行链接(computed 懒链接)。

  2. 若 link.version === -1,则执行下面逻辑(version 后面再说): 同步 link.version = this.version,然后若 link 存在 nextDep(If this dep has a next, it means it’s not at the tail),将其移动至尾部,原因注释中写了。

比如在 refImpl 的 get value()触发时就会触发 this.dep.track(), 将 refImpl 和当前的 activeSub 链接起来。

trigger 的内容如下:

trigger(debugInfo?: DebuggerEventExtraInfo): void {
  this.version++
  globalVersion++
  this.notify(debugInfo)
}

notify(debugInfo?: DebuggerEventExtraInfo): void {
  startBatch()
  try {
    if (__DEV__) {
      // subs are notified and batched in reverse-order and then invoked in
      // original order at the end of the batch, but onTrigger hooks should
      // be invoked in original order here.
      for (let head = this.subsHead; head; head = head.nextSub) {
        if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
          head.sub.onTrigger(
            extend(
              {
                effect: head.sub,
              },
              debugInfo,
            ),
          )
        }
      }
    }
    for (let link = this.subs; link; link = link.prevSub) {
      if (link.sub.notify()) {
        // if notify() returns `true`, this is a computed. Also call notify
        // on its dep - it's called here instead of inside computed's notify
        // in order to reduce call stack depth.
        ;(link.sub as ComputedRefImpl).dep.notify()
      }
    }
  } finally {
    endBatch()
  }
}

对于 computed,这里的 notify 内容如下:

/**
 * @internal
 */
notify(): true | void {
  this.flags |= EffectFlags.DIRTY
  if (
    // 确保一个更新周期内只被通知一次
    !(this.flags & EffectFlags.NOTIFIED) &&
    // avoid infinite self recursion
    activeSub !== this
  ) {
    batch(this, true)
    return true
  } else if (__DEV__) {
    // TODO warn
  }
}

这里 notify 触发的 sub.notify 实现如下:

export class ReactiveEffect<T = any>
  implements Subscriber, ReactiveEffectOptions
{
  ...
  notify(): void {
    if (
      this.flags & EffectFlags.RUNNING &&
      !(this.flags & EffectFlags.ALLOW_RECURSE)
    ) {
      return
    }
    if (!(this.flags & EffectFlags.NOTIFIED)) {
      batch(this)
    }
  }
}

实际就是将很多个副作用放在一个 bench 中执行,说是提升性能表现。bench 中实际相关触发副作用的代码实现如下:

export function endBatch(): void {
  if (--batchDepth > 0) {
    return;
  }

  if (batchedComputed) {
    let e: Subscriber | undefined = batchedComputed;
    batchedComputed = undefined;
    while (e) {
      const next: Subscriber | undefined = e.next;
      e.next = undefined;
      e.flags &= ~EffectFlags.NOTIFIED;
      e = next;
    }
  }

  let error: unknown;
  while (batchedSub) {
    let e: Subscriber | undefined = batchedSub;
    batchedSub = undefined;
    while (e) {
      const next: Subscriber | undefined = e.next;
      e.next = undefined;
      e.flags &= ~EffectFlags.NOTIFIED;
      if (e.flags & EffectFlags.ACTIVE) {
        try {
          // ACTIVE flag is effect-only
          (e as ReactiveEffect).trigger();
        } catch (err) {
          if (!error) error = err;
        }
      }
      e = next;
    }
  }

  if (error) throw error;
}

注意到实际触发的是(e as ReactiveEffect).trigger(),其代码实现如下:

trigger(): void {
  if (this.flags & EffectFlags.PAUSED) {
    // pausedQueueEffects是weakSet
    pausedQueueEffects.add(this)
  } else if (this.scheduler) {
    // 这里调度器作用就是延迟触发,修改触发时机之类的。
    this.scheduler()
  } else {
    this.runIfDirty()
  }
}

runIfDirty 与版本计数相关。

要理解 version 相关的内容就需要了解版本计数原理:

  • Dep 构造时会将 version 设置为 0,构造 Link 时会将 Link 的值设置为与 dep.version 相同。每次 trigger 触发时(比如 ref{set value()}) version 和 globalVersion 自增(globalVersion 是一个全局变量)。

  • 由前文可知 notify 最终会走到 runIfDirty,相关的代码实现如下:

/**
 * @internal
 */
runIfDirty(): void {
  if (isDirty(this)) {
    this.run()
  }
}

function isDirty(sub: Subscriber): boolean {
  for (let link = sub.deps; link; link = link.nextDep) {
    if (
      link.dep.version !== link.version ||
      (link.dep.computed &&
        (refreshComputed(link.dep.computed) ||
          link.dep.version !== link.version))
    ) {
      return true
    }
  }
  // @ts-expect-error only for backwards compatibility where libs manually set
  // this flag - e.g. Pinia's testing module
  if (sub._dirty) {
    return true
  }
  return false
}

分析 isDirty 里面的代码:link.dep.version !== link.version 在对于普通响应式对象时必定为 true 了(trigger 那里 dep.version++导致的 version 不相同)。对于 computed 对象由于其依赖更新不会触发其 version 自增故前一个判断不满足,需要 refreshCoputed 后进行判断。

/**
 * Returning false indicates the refresh failed
 * @internal
 */
export function refreshComputed(computed: ComputedRefImpl): undefined {
  if (
    computed.flags & EffectFlags.TRACKING &&
    !(computed.flags & EffectFlags.DIRTY)
  ) {
    return;
  }
  computed.flags &= ~EffectFlags.DIRTY;

  // Global version fast path when no reactive changes has happened since
  // last refresh.
  if (computed.globalVersion === globalVersion) {
    return;
  }
  computed.globalVersion = globalVersion;

  // In SSR there will be no render effect, so the computed has no subscriber
  // and therefore tracks no deps, thus we cannot rely on the dirty check.
  // Instead, computed always re-evaluate and relies on the globalVersion
  // fast path above for caching.
  // #12337 if computed has no deps (does not rely on any reactive data) and evaluated,
  // there is no need to re-evaluate.
  if (
    !computed.isSSR &&
    computed.flags & EffectFlags.EVALUATED &&
    ((!computed.deps && !(computed as any)._dirty) || !isDirty(computed))
  ) {
    return;
  }

  // 主要内容从这里开始
  computed.flags |= EffectFlags.RUNNING;

  // 准备上下文环境
  const dep = computed.dep;
  const prevSub = activeSub;
  const prevShouldTrack = shouldTrack;
  activeSub = computed;
  shouldTrack = true;

  try {
    prepareDeps(computed);
    const value = computed.fn(computed._value);
    // hasChanged eqs !Object.is(value, oldValue)
    if (dep.version === 0 || hasChanged(value, computed._value)) {
      computed.flags |= EffectFlags.EVALUATED;
      computed._value = value;
      dep.version++;
    }
  } catch (err) {
    dep.version++;
    throw err;
  } finally {
    // 恢复上下文环境
    activeSub = prevSub;
    shouldTrack = prevShouldTrack;
    cleanupDeps(computed);
    computed.flags &= ~EffectFlags.RUNNING;
  }
}

function prepareDeps(sub: Subscriber) {
  // Prepare deps for tracking, starting from the head
  for (let link = sub.deps; link; link = link.nextDep) {
    // set all previous deps' (if any) version to -1 so that we can track
    // which ones are unused after the run
    link.version = -1;
    // store previous active sub if link was being used in another context
    link.prevActiveLink = link.dep.activeLink;
    link.dep.activeLink = link;
  }
}

function cleanupDeps(sub: Subscriber) {
  // Cleanup unsued deps
  let head;
  let tail = sub.depsTail;
  let link = tail;
  while (link) {
    const prev = link.prevDep;
    if (link.version === -1) {
      if (link === tail) tail = prev;
      // unused - remove it from the dep's subscribing effect list
      removeSub(link);
      // also remove it from this effect's dep list
      removeDep(link);
    } else {
      // The new head is the last node seen which wasn't removed
      // from the doubly-linked list
      head = link;
    }

    // restore previous active link if any
    link.dep.activeLink = link.prevActiveLink;
    link.prevActiveLink = undefined;
    link = prev;
  }
  // set the new head & tail
  sub.deps = head;
  sub.depsTail = tail;
}

这里就比较浅显易懂了,准备上下文,将 sub 维度对应的链的 version 设为-1,执行计算,涉及到的依赖根据上文 version 会和 computed 同步,剩下的未涉及的链表结点通过 cleanupDeps 清理。注意这里如果 value 变了会让 computed.dep.version++,在上文中的 isDirty 中就会检查出来时 Dirty 的,然后就会执行相应的逻辑。

接上上上上文,isDirty 判断为 true 后执行 this.run,具体代码如下:

run(): T {
  if (!(this.flags & EffectFlags.ACTIVE)) {
    return this.fn()
  }

  this.flags |= EffectFlags.RUNNING
  cleanupEffect(this)
  prepareDeps(this)
  const prevEffect = activeSub
  const prevShouldTrack = shouldTrack
  activeSub = this
  shouldTrack = true

  try {
    return this.fn()
  } finally {
    if (__DEV__ && activeSub !== this) {
      warn(
        'Active effect was not restored correctly - ' +
          'this is likely a Vue internal bug.',
      )
    }
    cleanupDeps(this)
    activeSub = prevEffect
    shouldTrack = prevShouldTrack
    this.flags &= ~EffectFlags.RUNNING
  }
}

OK,到此为止吧