vue3 响应式原理:Ref

211 阅读2分钟

首先引入 ref​ 和 effect​ 两个函数,之后声明 name​ 响应式数据,接着又执行 effect​ 函数,该函数传入了一个匿名函数,最后两秒后又修改 name​ 值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      import {
        effect,
        ref
      } from "./reactivity.js";

      const count = ref(1);

      effect(() => {
        document.querySelector("#app").innerHTML = count.value;
      });

      setTimeout(() => {
        count.value++;
      }, 1000);
    </script>
  </body>
</html>

ref 实现

export function ref(value?: unknown) {
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
// 创建RefTmpl类
  return new RefImpl(rawValue, shallow)
}

​ref​ 函数实际执行的是 createRef​ 方法,而该方法实际是返回了一个 RefImpl​ 构造函数的实例对象:

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    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)) {
      const oldVal = this._rawValue
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal)
    }
  }
}

​RefImpl​ 构造函数会接收传入的值,可能是基本类型也可能是复杂类型,通过 _rawValue​ 记录原始值,用于之后依赖触发时新旧值的比较,我们需关注 this._value = __v_isShallow ? value : toReactive(value)​,如果已经是响应式对象,无需再次赋值

1、依赖收集

export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffect(
      activeEffect,
      (ref.dep ??= createDep(
        () => (ref.dep = undefined),
        ref instanceof ComputedRefImpl ? ref : undefined,
      )),
      void 0,
    ) // 逻辑同reactive
  }
}

这块逻辑同 reactive​,给指定属性绑定对应的 fn​,目的是 dep​ 对象与 ReactiveEffect​ 相关联,完成整个依赖收集的过程。

2、派发更新

export function triggerRefValue(
  ref: RefBase<any>,
  dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
  newVal?: any,
  oldVal?: any,
) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    triggerEffects(dep, dirtyLevel, void 0)
  }
}

上篇 reactive​ 我们也分析了这块逻辑,最终执行的是每个 effect.run​ 方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成

总结

  1. ​ref​ 函数本质上做了三件事:一是返回 RefImpl​ 的实例;二是对数据处理,如果当前数据为基本类型,则直接返回;如果为复杂类型,则调用 reactive​ 返回 reactive​ 数据;三是 RefImpl​ 提供 get value​ 和 set value​ 方法,这就是为什么设置 ref​ 值时,需要带上 .value​。
  2. ​ref​ 基本类型的数据不具备数据监听,赋值或修改值都是主动触发 get​ 和 set​ 方法。
  3. 为什么 ref​ 类型数据,必须要通过 .value​ 访问值呢?
    a. 因为 ref​ 需要处理基本数据类型的响应性,但是对于基本类型数据而言,它无法通过 proxy​ 建立代理。
    b. 而 vue​ 通过 get value()​ 和 set value()​ 定义了两个属性函数,通过主动触发这两个函数(属性调用)的形式来进行依赖收集和依赖触发。
    c. 所以我们必须通过 .value​ 来保证响应性。