vue3响应式原理:ref、computed、watch和render的关系

370 阅读10分钟

我们日常开发过程中经常会用到响应式数据ref、计算属性computed和侦听器watch。我们知道,他们共同点是在数据发生变化的时候,总会引起一些其他变化:ref对应的数据变化可能会引起视图变化、计算属性变化或侦听器的事件触发;computed变化可能可能会引起视图变化;而watch则是那个监听响应式数据变化的api

简言之,一个变化会引起另一个或另几个的变化。接下来我们就一步步探索这几个api底层的实现以及他们之间千丝万缕的关系。

先来看一个例子:

<template>
  <div>count:{{ count }}</div>
  <div>hasUnitCount:{{ hasUnitCount }}</div>
  <button @click="increaseCount">increaseCount</button>
</template>
<script setup>
import { computed, ref, watch } from "vue";
// 响应式数据ref
const count = ref(1);
// 计算属性computed
const hasUnitCount = computed(() => {
  return count.value + "个";
});
// 侦听器watch
watch(count, (newValue, oldValue) => {
  console.log(newValue, oldValue);
});
const increaseCount = () => {
  count.value = count.value * 2;
};
</script>

以上例子中,我们涵盖了refcomputedwatch三个api的使用,为了解释方便,我们简单探讨一下经典的发布订阅者模式

一、发布订阅模式的定义

有一条古老的街道,
某天,街上开了一家财经报社(发布者)张三(订阅者)去订阅了一年的报纸,并且留下了地址,从此报社每天都会派送报员给张三送报纸, 某天,李四(订阅者)也订阅了报纸,报社的订阅者就变成了 2 个(发布者的 dep 就变成了 2)。

某天,街上又开了一家体育报社(发布者)张三(订阅者)又订阅了一年的报纸,此时,张三 所依赖的发布者就变成了 2 个(订阅者的 deps 就变成了 2)。

故事很短, 但可以就发布订阅者有一个概括性的认识。发布者是消息的发出者,订阅者是消息的接收者。一个发布者可以被多个订阅者订阅,一个订阅者也可以订阅多个发布者的消息。

image.png

上图可以清晰的看出依赖收集和派发更新时数据或信息的流向。vue3的响应式原理就是以发布订阅者模式为核心设计模式实现的,下面开始讨论vue3中的发布订阅者。

二、vue3中的发布者和订阅者

1、ref-发布者

// ref函数
function ref(value) {
  return createRef(value, false);
}
// createRef函数
function createRef(rawValue, shallow) {
  // 如果是ref,直接返回
  if (isRef(rawValue)) {
    return rawValue;
  }
  // 创建RefImpl实例
  return new RefImpl(rawValue, shallow);
}
// 定义RefImpl类(包含get和set函数)
class RefImpl {
  constructor(value, __v_isShallow) {
    this.__v_isShallow = __v_isShallow;
    this.dep = void 0;
    this.__v_isRef = true;
    this._rawValue = __v_isShallow ? value : toRaw(value);
    this._value = __v_isShallow ? value : toReactive(value);
  }
  get value() {
    // 依赖收集:收集订阅者
    trackRefValue(this);
    return this._value;
  }
  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
    newVal = useDirectValue ? newVal : toRaw(newVal);
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = useDirectValue ? newVal : toReactive(newVal);
      // 派发更新:将数据变化的消息发送给订阅者
      triggerRefValue(this, 4, newVal);
    }
  }
}

以上是响应式 apiref的基本逻辑,最终是实例化了RefImpl,当访问到该数据时,会通过trackRefValue来实现依赖收集,当修改其数据时,又会通过triggerRefValue的方式进行派发更新。

computedwatch都可以以ref对应的响应式数据的变化作为变化依据,所以,我们可以认为ref对应的数据家是一个发布者

2、computed-即是发布者,又是订阅者

// 定义computed入口
const computed = (getterOrOptions, debugOptions) => {
  const c = computed$1(getterOrOptions, debugOptions, isInSSRComponentSetup);
  if (!!(process.env.NODE_ENV !== "production")) {
    const i = getCurrentInstance();
    if (i && i.appContext.config.warnRecursiveComputed) {
      c._warnRecursive = true;
    }
  }
  return c;
};
// 这个computed就是以上的computed$1
function computed(getterOrOptions, debugOptions, isSSR = false) {
  let getter;
  let setter;
  const onlyGetter = isFunction(getterOrOptions);
  if (onlyGetter) {
    getter = getterOrOptions;
    setter = !!(process.env.NODE_ENV !== "production")
      ? () => {
          warn("Write operation failed: computed value is readonly");
        }
      : NOOP;
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  const cRef = new ComputedRefImpl(
    getter,
    setter,
    onlyGetter || !setter,
    isSSR
  );
  if (!!(process.env.NODE_ENV !== "production") && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack;
    cRef.effect.onTrigger = debugOptions.onTrigger;
  }
  return cRef;
}
// 定义ComputedRefImpl类(包含get和set函数)
class ComputedRefImpl {
  constructor(getter, _setter, isReadonly, isSSR) {
    this.getter = getter;
    this._setter = _setter;
    this.dep = void 0;
    this.__v_isRef = true;
    this["__v_isReadonly"] = false;
    // 创建ReactiveEffect实例
    this.effect = new ReactiveEffect(
      () => getter(this._value),
      () => triggerRefValue(this, this.effect._dirtyLevel === 2 ? 2 : 3)
    );
    this.effect.computed = this;
    this.effect.active = this._cacheable = !isSSR;
    this["__v_isReadonly"] = isReadonly;
  }
  get value() {
    const self = toRaw(this);
    if (
      (!self._cacheable || self.effect.dirty) &&
      hasChanged(self._value, (self._value = self.effect.run()))
    ) {
      // 派发更新:将数据变化的消息发送给订阅者
      triggerRefValue(self, 4);
    }
    // 依赖收集:收集订阅者
    trackRefValue(self);
    if (self.effect._dirtyLevel >= 2) {
      if (!!(process.env.NODE_ENV !== "production") && this._warnRecursive) {
        warn(COMPUTED_SIDE_EFFECT_WARN, `getter: `, this.getter);
      }
      // 派发更新:将数据变化的消息发送给订阅者
      triggerRefValue(self, 2);
    }
    return self._value;
  }
  set value(newValue) {
    this._setter(newValue);
  }
  get _dirty() {
    return this.effect.dirty;
  }
  set _dirty(v) {
    this.effect.dirty = v;
  }
}

以上是计算属性 apicomputed的基本逻辑,最终是实例化了ComputedRefImpl,当访问到该计算属性时,会通过trackRefValue来实现依赖收集,当其依赖的数据变化时,又会通过triggerRefValue的方式进行派发更新。

计算属性对应的数据变化会引起视图的变化,我们可以认为计算属性对应的数据是发布者

它自身又实现了ReactiveEffect的实例effect,也称之为computed-effect,它是可以被收集的。所以,我们称computed也是订阅者

3、watch-订阅者

function watch(source, cb, options) {
  // 如果cb不是函数,控制台打印警告
  if (!!(process.env.NODE_ENV !== "production") && !isFunction(cb)) {
    warn$1(
      `\`watch(fn, options?)\` signature has been moved to a separate API. Use \`watchEffect(fn, options?)\` instead. \`watch\` now only supports \`watch(source, cb, options?) signature.`
    );
  }
  return doWatch(source, cb, options);
}
function doWatch(
  source,
  cb,
  { immediate, deep, flush, once, onTrack, onTrigger } = EMPTY_OBJ
) {
  let getter;
  let forceTrigger = false;
  let isMultiSource = false;
  // 1、通过source获取getter
  // source是ref
  if (isRef(source)) {
    getter = () => source.value;
    forceTrigger = isShallow(source);
    // source是reactive
  } else if (isReactive(source)) {
    getter = () => reactiveGetter(source);
    forceTrigger = true;
    // source是数组
  } else if (isArray(source)) {
    isMultiSource = true;
    forceTrigger = source.some((s) => isReactive(s) || isShallow(s));
    getter = () =>
      source.map((s) => {
        if (isRef(s)) {
          return s.value;
        } else if (isReactive(s)) {
          return reactiveGetter(s);
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, 2);
        } else {
          !!(process.env.NODE_ENV !== "production") && warnInvalidSource(s);
        }
      });
    // source是函数
  } else if (isFunction(source)) {
    if (cb) {
      getter = () => callWithErrorHandling(source, instance, 2);
    } else {
      getter = () => {
        if (cleanup) {
          cleanup();
        }
        return callWithAsyncErrorHandling(source, instance, 3, [onCleanup]);
      };
    }
    // 否则为空函数
  } else {
    getter = NOOP;
    !!(process.env.NODE_ENV !== "production") && warnInvalidSource(source);
  }
  if (cb && deep) {
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }
  let cleanup;
  let onCleanup = (fn) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn, instance, 4);
      cleanup = effect.onStop = void 0;
    };
  };
  let oldValue = isMultiSource
    ? new Array(source.length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE;
  // 2、定义scheduler
  const job = () => {
    if (!effect.active || !effect.dirty) {
      return;
    }
    if (cb) {
      const newValue = effect.run();
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? newValue.some((v, i) => hasChanged(v, oldValue[i]))
          : hasChanged(newValue, oldValue)) ||
        false
      ) {
        if (cleanup) {
          cleanup();
        }
        callWithAsyncErrorHandling(cb, instance, 3, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE
            ? void 0
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
          onCleanup,
        ]);
        oldValue = newValue;
      }
    } else {
      effect.run();
    }
  };
  job.allowRecurse = !!cb;
  let scheduler;
  if (flush === "sync") {
    scheduler = job;
  } else if (flush === "post") {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
  } else {
    // 用于后续事件优先级的排序(在后续流程中用到,pre表示watch在job.id相同的时候有更高的执行优先级)
    job.pre = true;
    if (instance) job.id = instance.uid;
    scheduler = () => queueJob(job);
  }
  // 3、创建ReactiveEffect实例
  const effect = new ReactiveEffect(getter, NOOP, scheduler);
  const scope = getCurrentScope();
  const unwatch = () => {
    effect.stop();
    if (scope) {
      remove(scope.effects, effect);
    }
  };
  // 4、执行job/effect.run()
  if (cb) {
    if (immediate) {
      job();
    } else {
      // 当前例子中执行effect.run()
      oldValue = effect.run();
    }
  }
  // 5、返回unwatch
  return unwatch;
}

这里主要根据条件获取getterscheduler,然后通过const effect = new ReactiveEffect(getter, NOOP, scheduler)创建侦听器effect,也称之为watch-effect,它是可以被收集的。所以,我们称watch也是订阅者

4、render-订阅者

这里不得不提一个渲染对应的effect,先称其为render-effect,我们省略其他逻辑,主要列出其流程代码:

const componentUpdateFn = () => {
  // 获取实例的vnode
  instance.subTree = renderComponentRoot(instance);
  // 渲染实例的视图
  patch(null, subTree, container, anchor, instance, parentSuspense, namespace);
};
const effect = (instance.effect = new ReactiveEffect(
  componentUpdateFn,
  NOOP,
  () => queueJob(update),
  instance.scope
));

视图会根据响应式数据refcomputed的变化而更新,所以,它也是名副其实的订阅者

5、小结

先根据以上内容对refcomputedwatchrender做个对比。

image.png

可以看出,ref仅仅作为发布者,可以对computedwatchrender进行收集。computed比较特殊,它既可以作为发布者被render订阅,也可作为订阅者订阅ref消息或数据。watchrender的特点一致,都只能作为订阅者来订阅其他发布者的消息或数据。

三、vue3中发布者收集订阅者

vue3的详细流程可以参考寻找 vue3 源码脉络图。这里只介绍与本例有关的主要流程:
这里首先介绍主要的ReactiveEffect

// 定义当前激活状态的effect
let activeEffect;
// 定义ReactiveEffect类
class ReactiveEffect {
  constructor(fn, trigger, scheduler, scope) {
    this.fn = fn;
    this.trigger = trigger;
    this.scheduler = scheduler;
    this.active = true;
    this.deps = [];
    this._dirtyLevel = 4;
    this._trackId = 0;
    this._runnings = 0;
    this._shouldSchedule = false;
    this._depsLength = 0;
    recordEffectScope(this, scope);
  }
  get dirty() {
    if (this._dirtyLevel === 2 || this._dirtyLevel === 3) {
      this._dirtyLevel = 1;
      pauseTracking();
      for (let i = 0; i < this._depsLength; i++) {
        const dep = this.deps[i];
        if (dep.computed) {
          triggerComputed(dep.computed);
          if (this._dirtyLevel >= 4) {
            break;
          }
        }
      }
      if (this._dirtyLevel === 1) {
        this._dirtyLevel = 0;
      }
      resetTracking();
    }
    return this._dirtyLevel >= 4;
  }
  set dirty(v) {
    this._dirtyLevel = v ? 4 : 0;
  }
  run() {
    this._dirtyLevel = 0;
    if (!this.active) {
      return this.fn();
    }
    let lastShouldTrack = shouldTrack;
    // 记录上一个activeEffect
    let lastEffect = activeEffect;
    try {
      shouldTrack = true;
      // 将当前effect赋值给activeEffect,在执行fn时,必要的话可以对当前的activeEffect进行收集
      activeEffect = this;
      this._runnings++;
      preCleanupEffect(this);
      // 执行fn
      return this.fn();
    } finally {
      postCleanupEffect(this);
      this._runnings--;
      // 将effect重置为上一次的activeEffect
      activeEffect = lastEffect;
      shouldTrack = lastShouldTrack;
    }
  }
  stop() {
    var _a;
    if (this.active) {
      preCleanupEffect(this);
      postCleanupEffect(this);
      (_a = this.onStop) == null ? void 0 : _a.call(this);
      this.active = false;
    }
  }
}

以上代码中主要关注run函数,它执行fn的过程中,可能会访问到响应式apirefcomputed对应的数据,也就访问到了其get函数,进而触发trackRefValue依赖收集的过程。

这里在执行fn之前,执行了activeEffect = this,所以,如果收集的话,就是收集的其自身effect

computedwatchrender中都是以该ReactiveEffect为共同类,又根据其自身不同的特定实例化各自对应的effect
再看依赖收集函数:

function trackRefValue(ref2) {
  var _a;
  if (shouldTrack && activeEffect) {
    ref2 = toRaw(ref2);
    trackEffect(
      activeEffect,
      (_a = ref2.dep) != null
        ? _a
        : (ref2.dep = createDep(
            () => (ref2.dep = void 0),
            ref2 instanceof ComputedRefImpl ? ref2 : void 0
          )),
      !!(process.env.NODE_ENV !== "production")
        ? {
            target: ref2,
            type: "get",
            key: "value",
          }
        : void 0
    );
  }
}
function trackEffect(effect2, dep, debuggerEventExtraInfo) {
  var _a;
  if (dep.get(effect2) !== effect2._trackId) {
    dep.set(effect2, effect2._trackId);
    const oldDep = effect2.deps[effect2._depsLength];
    if (oldDep !== dep) {
      if (oldDep) {
        cleanupDepEffect(oldDep, effect2);
      }
      effect2.deps[effect2._depsLength++] = dep;
    } else {
      effect2._depsLength++;
    }
    if (!!(process.env.NODE_ENV !== "production")) {
      (_a = effect2.onTrack) == null
        ? void 0
        : _a.call(effect2, extend({ effect: effect2 }, debuggerEventExtraInfo));
    }
  }
}

trackRefValue中将activeEffectref2.dep作为参数传入。

trackEffect中将当前激活的effect2作为keyeffect2._trackId作为value收集到dep中。同时,通过effect2.deps[effect2._depsLength++] = dep的方式将dep记录到deps中去。

至此,响应式数据ref和各个effect之间的关系完成建立。发布者收集了effect,通过dep记录各个订阅者的信息;订阅者也将其订阅的发布者信息记录在deps中。

1、setup阶段

setupComponent(instance) --> setupStatefulComponent(instance, isSSR) --> setup

在这个流程中:

实例化了计算属性的ComputedRefImpl,并且为其实例化了activeEffect--computed-effect

实例化了watch-effect,并且完成了ref对于watch-effect的收集。

2、setupRenderEffect阶段

实例化了渲染的activeEffect--render-effect

3、subTree阶段

执行const subTree = instance.subTree = renderComponentRoot(instance)获取组件的vnode阶段,访问到了refcomputed对应的数据,在这个流程中:

完成了RefImpl对于render-effect的收集。

完成了RefImpl对于computed-effect的收集。

完成了ComputedRefImpl对于render-effect的收集。

篇幅有限,如果需要了解响应式依赖收集和派发更新的详细流程,可以移步:
响应式数据详情可查看vue3响应式原理:reactive
计算属性详情可查看vue3响应式原理:可被收集/也可被收集的computed
侦听器详情可查看vue3响应式原理:被监听的watch

四、vue3中发布者通知订阅者

当我们点击例子中的按钮,触发数据变化时,会让视图重新渲染。

const increaseCount = () => {
  count.value++;
};

修改count.value会触发RefImplget函数:

function set(newVal) {
  const useDirectValue =
    this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
  newVal = useDirectValue ? newVal : toRaw(newVal);
  if (hasChanged(newVal, this._rawValue)) {
    this._rawValue = newVal;
    this._value = useDirectValue ? newVal : toReactive(newVal);
    triggerRefValue(this, 4, newVal);
  }
}
function triggerRefValue(ref2, dirtyLevel = 4, newVal) {
  ref2 = toRaw(ref2);
  const dep = ref2.dep;
  // 如果有收集到的dep,执行triggerEffects
  if (dep) {
    triggerEffects(
      dep,
      dirtyLevel,
      !!(process.env.NODE_ENV !== "production")
        ? {
            target: ref2,
            type: "set",
            key: "value",
            newValue: newVal,
          }
        : void 0
    );
  }
}
function triggerEffects(dep, dirtyLevel, debuggerEventExtraInfo) {
  var _a;
  pauseScheduling();
  // 以dep的key值为数组遍历执行trigger或queueEffectSchedulers.push(effect2.scheduler)函数
  for (const effect2 of dep.keys()) {
    let tracking;
    if (
      effect2._dirtyLevel < dirtyLevel &&
      (tracking != null
        ? tracking
        : (tracking = dep.get(effect2) === effect2._trackId))
    ) {
      effect2._shouldSchedule ||
        (effect2._shouldSchedule = effect2._dirtyLevel === 0);
      effect2._dirtyLevel = dirtyLevel;
    }
    if (
      effect2._shouldSchedule &&
      (tracking != null
        ? tracking
        : (tracking = dep.get(effect2) === effect2._trackId))
    ) {
      if (!!(process.env.NODE_ENV !== "production")) {
        (_a = effect2.onTrigger) == null
          ? void 0
          : _a.call(
              effect2,
              extend({ effect: effect2 }, debuggerEventExtraInfo)
            );
      }
      // 触发effet2的派发更新
      effect2.trigger();
      if (
        (!effect2._runnings || effect2.allowRecurse) &&
        effect2._dirtyLevel !== 2
      ) {
        effect2._shouldSchedule = false;
        if (effect2.scheduler) {
          // 如果有计划中的函数scheduler,推入到queueEffectSchedulers中去
          queueEffectSchedulers.push(effect2.scheduler);
        }
      }
    }
  }
  resetScheduling();
}
// 执行完事件派发更新后
function resetScheduling() {
  pauseScheduleStack--;
  while (!pauseScheduleStack && queueEffectSchedulers.length) {
    queueEffectSchedulers.shift()();
  }
}

最终会执行到effect2.trigger(),也可能执行到queueEffectSchedulers.push(effect2.scheduler)的流程,至于后续的流程请自行断点调试。如果有疑问,可以评论区留言,我再补充完善。

这里主要介绍了refcomputedwatchrender之间的关系,它们可以简单的归为发布者或订阅者,关系是收集和被收集,信息或数据流向是指向收集和通知的方向,整个响应式体系围绕着发布订阅者模式展开。