Vue源码学习(ref)

245 阅读4分钟

前言

上节讲了 vue 的核心响应式原理,这节讲一下 ref 的实现,以及与ref 相关的一些 api

refvue 提供的一个函数,可以用来包装原始值,使其具有响应性。

ref

export function ref(value) {
  return createRef(value);
}

function createRef(value) {
  return new RefImpl(value);
}

ref 函数的实现很简单,就是创建一个 RefImpl 对象。

我们看一下这个对象的实现。

class RefImpl {
  public __v_isRef = true; // 标记是否是 ref 对象

  public _value; // 存储 ref 的 value 值

  public dep; // 用于收集依赖

  constructor(public rawValue) {
    // 如果 rawValue 是一个对象,将 rawValue 转为响应性对象
    this._value = convert(rawValue);
    this.dep = createDep(() => (this.dep = void 0));
  }

  get value() {
    // 收集依赖
    trackRefValue(this);

    return this._value;
  }

  set value(newValue) {
    if (newValue !== this.rawValue) {
      this.rawValue = newValue;
      this._value = newValue;

      // 触发更新
      triggerRefValue(this);
    }
  }
}

RefImpl类有几个属性:

  • __v_isRef:标记是否是 ref 对象。
  • _value:存储 ref 的值。
  • dep:用于收集依赖。
  • rawValue:传入的原始值。

在构造函数里面,会将传入的 rawValue 传入到 convert 函数中。

convert 函数实现如下:

export function convert(value) {
  return isObject(value) ? reactive(value) : value;
}

这个函数很简单,就是判断传入的 rawValue 是否是一个对象,如果是,则调用 reactive 函数将其转为响应性对象,不是就直接返回这个值。

然后,会调用 createDep 创建一个 Map,赋值给 dep属性,用于收集依赖。之后收集到的 effect 对象都会被存入 dep 中。

RefImpl 类有一个 getset 方法,用来拦截 valuegetset 操作。

get 方法中,会调用 trackRefValue 收集依赖。并返回 _value 属性。

set 方法中,会判断传入的 newValue 是否和 rawValue 不同,如果不同,则更新 rawValue_value 属性,并调用 triggerRefValue 触发更新。

接下来我们看一下 trackRefValuetriggerRefValue 函数:

export function trackRefValue(ref) {
  if (activeEffect) {
    trackEffects(activeEffect, ref.dep);
  }
}
export function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep);
  }
}

着两个函数也很简单,就是调用 trackEffectstriggerEffects 函数。

简单来讲, ref 函数就是创建一个对象,通过拦截对象身上的 value 属性,来实现依赖的收集和更新。

ref 相关 API

ref 函数的实现还是比较简单的,但是 ref 有很多 api 需要我们了解,如果对用法不熟悉的可以去看一下官方文档。

接下来我们看一下 ref 相关的 api

isRef

export function isRef(value) {
  return !!(value && value.__v_isRef);
}

这个没什么好讲的,就是判断传入的 value 身上是否有 __v_isRef 属性。

unRef

export function unRef(ref) {
  return isRef(ref) ? ref.value : ref;
}

这个函数也很简单,就是判断传入的 ref 是否是一个 ref 对象,如果是,则返回 refvalue 属性,否则返回这个值。

toRef

export function toRef(object, key) {
  return new ObjectRefImpl(object, key);
}

这个函数是用来对响应式对象上的某个属性,创建一个对应的 ref, 可以看到 toRef 函数返回的是一个 ObjectRefImpl 对象。

class ObjectRefImpl {
  public __v_isRef = true; // 标记是否是 ref 对象

  constructor(public object, public key) {}

  get value() {
    return this.object[this.key];
  }

  set value(newValue) {
    this.object[this.key] = newValue;
  }
}

这个类也很简单,就是包装了一个对象,并拦截了 value 属性的 getset 方法。由于传进来的对象本身就是个响应式的,所以访问对象身上的属性,会自动触发依赖收集。

toRefs

export function toRefs(object) {
  const res = {};

  // 遍历 object 的 key,将每个 key 转为 ref 对象
  for (const key in object) {
    res[key] = toRef(object, key);
  }

  return res;
}

这个函数是用来对响应式对象上的所有属性,创建一个对应的 ref 对象。

具体实现就是遍历对象上的所有属性,将每个属性转为 ref 对象,并将其存入一个新的对象中,并返回这个新的对象。

proxyRefs

这个函数在 vue 的官方 api 并没有,他的具体作用是可以不需要调用 ref 对象的 value 属性,直接访问 ref 对象上的属性。

其实在 template 中,我们可以直接访问 ref 对象上的属性,这是因为在 setup 函数的返回值中会自动用 proxyRefs 函数包装,使得 ref 对象上的属性可以在模版中直接访问,而不需要 .value 后缀。

export function proxyRefs(objectWithRef) {
  return new Proxy(objectWithRef, {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver);
      return unRef(res);
    },

    set(target, key, value, receiver) {
      const oldValue = target[key];
      if (oldValue !== value) {
        if (isRef(oldValue)) {
          target[key].value = value;
          return target[key];
        } else {
          return Reflect.set(target, key, value, receiver);
        }
      }
    },
  });
}

这个函数的实现也很简单,就是创建一个 Proxy 对象,对传进来的 ref 对象做一个代理。

当访问 ref 对象上的属性时,会调用 get 方法,在 get 方法中,会调用 unRef 函数,返回 ref 对象上的 value 属性。

当修改 ref 对象上的属性时,会调用 set 方法,在 set 方法中,会判断 oldValue 是否是一个 ref 对象,如果是, 就修改它的 value 属性,否则直接修改改属性。

至此,ref 的实现和相关的 api 就介绍完了。