likes
comments
collection
share

vue3响应式原理:可被收集/也可被收集的computed

作者站长头像
站长
· 阅读数 16

计算属性指的是,数据是由其他数据的复杂计算所得。以下是computed相关的依赖收集和派发更新的实现原理:

先看个例子:

<template>
  <div> {{ doubleCount }} </div>
  <button @click="increaseCount">increaseCount</button>
</template>
<script setup>
  import { ref, computed } from 'vue'
  // 定义响应式
  const count = ref(0)
  // 定义计算属性
  const doubleCount = computed(() => {
    return count.value * 3
  })
  // 修改数据:引起计算属性变化,再引起视图变化
  const increaseCount = () => {
    count.value++;
  }
</script>

一、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的方式进行派发更新。

二、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);
  }
  // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
  get _dirty() {
    return this.effect.dirty;
  }
  set _dirty(v) {
    this.effect.dirty = v;
  }
}

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

三、依赖收集流程

创造类:ReactiveEffect 中的 run 为:

run() {
    this._dirtyLevel = 0;
    if (!this.active) {
      return this.fn();
    }
    let lastShouldTrack = shouldTrack;
    let lastEffect = activeEffect;
    try {
      shouldTrack = true;
      activeEffect = this;
      this._runnings++;
      preCleanupEffect(this);
      return this.fn();
    } finally {
      postCleanupEffect(this);
      this._runnings--;
      activeEffect = lastEffect;
      shouldTrack = lastShouldTrack;
    }
  }

阶段 1:渲染 effect

当执行到setupRenderEffect逻辑时,执行流程为: update() --> effect.run() --> this.run()

重点关注activeEffect = this,这里就将渲染的effect作为当前激活状态的activeEffect

这里执行的this.fn()就是渲染组件的逻辑componentUpdateFn,其中由const subTree = instance.subTree = renderComponentRoot(instance)获取vnode,由patch进行渲染。

subTree中有主要逻辑:

result = normalizeVNode(
  render.call(thisProxy, proxyToUse, renderCache, props, setupState, data, ctx)
);

执行render函数,就会获取例子中的doubleCount,进而执行其get函数中的逻辑self._value = self.effect.run()

阶段 2:计算属性 effect

重点关注activeEffect = this,这里就将计算属性的effect作为当前激活状态的activeEffect

这里执行的this.fn()就是计算属性的逻辑() => getter(this._value),即() => { return count.value * 3 },访问到了count.value,会触发RefImpl对应的get函数get value() { trackRefValue(this); return this._value; }。其中rackRefValue(this)就是依赖收集的逻辑:

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和计算属性computed之间的关系完成建立。

run逻辑执行结束后,通过activeEffect = lastEffect将当前激活的effect赋值为渲染effect

阶段 3:渲染 effect

继续接着阶段 1 中的:

if (
  (!self._cacheable || self.effect.dirty) &&
  hasChanged(self._value, (self._value = self.effect.run()))
) {
  triggerRefValue(self, 4);
}
trackRefValue(self);

执行到依赖收集方法trackRefValue(self),也就是阶段 2 中的trackRefValuetrackEffect,这个阶段中,计算属性数据ComputedRefImpl和渲染effect之间的关系完成建立。

run逻辑执行结束后,通过activeEffect = lastEffect将当前激活的effect赋值为undefined

收集过程小结:subtree获取过程中触发计算属性doubleCount的求值 --> computed中的get --> 触发() => { count.value * 3} --> 触发count.value的依赖收集,收集的是computed计算属性 --> 触发trackRefValue(self)的执行,触发的是computed收集渲染effect

四、派发更新

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

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();
  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(),也就是() => triggerRefValue(this, this.effect._dirtyLevel === 2 ? 2 : 3)

下一个阶段的triggerRefValue会触发effect2.trigger(),也就是noop = () => {},最终会执行queueEffectSchedulers.push(effect2.scheduler)

再接着会执行到queueEffectSchedulers.shift()(),这里开始就是视图更新的逻辑。

总结:首次渲染时,按照渲染effect --> ComputedRefImpl --> RefImpl的顺序进行依赖收集;数据更新时按照RefImplcomputed渲染effect顺序进行派发更新,完成视图的更新。

转载自:https://juejin.cn/post/7390902948614963212
评论
请登录