Vue 3 源码解析(6)ref 的实现(下)

1,244 阅读3分钟

Vue3 源码系列文章会持续更新,全部文章请查阅我的掘金专栏

本系列的文章 demo 存放于我的 Github 仓库,推荐大家下载和调试,进而加深理解。

一. toRef 和 toRefs

通过 reactive 代理后的响应式对象是不支持解构的,例如下方的代码不会按预期执行:

<body>
  <div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
  const { reactive, effect } = Vue;
  
  const div = document.querySelector('div');
  const obj = reactive({msg: 'Hello!'});
  
  effect(() => {
    div.innerText = obj.msg
  });
  
  const { msg } = obj;  // 解构
  msg = 'Bye~';  // 不会触发副作用函数
</script>

toRef 和 toRefs 就是用来解决该问题的,它们可以把响应式对象内的单个或多个属性,转化为 ref 实例的形式,通过 value 属性来访问/修改对应的值,并触发相应的副作用函数。
像上面的代码可以利用这两个接口来处理:

<body>
  <div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
  const { reactive, effect, toRef, toRefs } = Vue;
  
  const div = document.querySelector('div');
  const obj = reactive({msg: 'Hello!'});
  
  effect(() => {
    div.innerText = obj.msg
  });
  
  const msg = toRef(obj, 'msg');  // 或者 const { msg } = toRefs(obj);
  msg.value = 'Bye~';  // 可以触发副作用函数
</script>

它们的实现比较简单,依旧是利用 getter 和 setter 来拦截 value 属性的请求 —— 当访问 value 属性时,返回响应式对象对应的属性值;当修改 value 属性时,直接修改响应式对象。
接口实现如下:

/** ref.js **/

export function toRef(object, key, defaultValue) {
    const val = object[key]
    return isRef(val)
        ? val
        : (new ObjectRefImpl(object, key, defaultValue))
}

export function toRefs(object) {
    const ret = isArray(object) ? [] : {}
    for (const key in object) {
        ret[key] = toRef(object, key)
    }
    return ret
}

class ObjectRefImpl {
    constructor(object, key, defaultValue) {  // defaultValue 是缺省值
        this.__v_isRef = true;
        this._object = object;
        this._key = key;
        this._defaultValue = defaultValue;
    }
    get value() {
        const val = this._object[this._key]
        return val === undefined ? this._defaultValue : val
    }
    set value(newVal) {
        this._object[this._key] = newVal
    }
}

着重关注 ObjectRefImpl 和 toRef 的实现即可,toRefs 不外乎是遍历传入的响应式对象,再复用 toRef 接口来映射全部的属性。

二. customRef

Vue 提供了一个自定义接口 customRef,可以让用户决定 ref 实例执行 track 和 trigger 的时机。
customRef 接收一个工厂函数为参数,该工厂函数又包含 track 和 trigger 两个函数类型的参数,且必须返回带有 get 和 set 属性方法的对象。
下方示例通过 customRef 接口,自定义了一个只接受偶数赋值的 ref

<body>
  <div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
  const { effect, customRef } = Vue;
  const div = document.querySelector('div');
  
  // 只接受偶数的 ref
  function useEvenRef(value = 0) {
    return customRef((track, trigger) => {
      return {
        get() {
          track();
          return value
        },
        set(newValue) {
          if (newValue % 2 === 0) {  // 偶数才执行
            value = newValue
            trigger()
          }
        }
      }
    })
  }
  
  let num = useEvenRef();
  
  effect(() => {
    div.innerText = num.value
  });
  
  let count = 0;
  setInterval(() => {
    ++count;
    num.value = count;
  }, 500);
</script>

customRef 的实现如下:

/** ref.js **/

export function customRef(factory) {
    return new CustomRefImpl(factory)
}

class CustomRefImpl {
    constructor(factory) {
        this.__v_isRef = true;
        const { get, set } = factory(
            () => trackRefValue(this),
            () => triggerRefValue(this)
        )
        this._get = get;
        this._set = set;
    }
    get value() {
        return this._get()
    }

    set value(newVal) {
        this._set(newVal)
    }
}

可以看到,在类 CustomRefImpl 中利用了闭包的能力,将传入工厂函数的执行结果(get 和 set)封存为内部属性,并拦截用户对 value 属性的访问和修改来调用 get 和 set方法。

另外在 CustomRefImpl 中也将工厂函数的两个参数定义为调用 trackRefValue 和 triggerRefValue 的方法,用户在外部执行这两个参数,即可调用依赖收集和触发副作用的能力。

常规要求在工厂函数返回的 get 中去调用 track,在 set 中去调用 trigger

三. proxyRefs

如果一个对象中存在某个属性指向 ref 实例,每次我们在使用时(无论是访问,抑或修改),都需要访问其 value 属性,这是一个开发环节的心智负担:

  const { ref, effect } = Vue;
  const div = document.querySelector('div');
  const info = ref('Hello');
  const obj = { info };
  
  effect(() => {
    div.innerText = obj.info.value;  // 需要多打一个 .value
  });
  
  obj.info.value = 'Bye~';  // 需要多打一个 .value

得益于 Proxy 的拦截机制,可以拦截用户对对象属性的访问,再在拦截器中返回/修改对应属性的 value 值即可。
我们基于该原理来新增一个 proxyRefs 接口:

/** ref.js **/

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

export function proxyRefs(objectWithRefs) {
    return new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

const shallowUnwrapHandlers = {
    get: (target, key, receiver) => {
      return unref(Reflect.get(target, key, receiver))
    },
    set: (target, key, value, receiver) => {
        const oldValue = target[key];

        if (isRef(oldValue) && !isRef(value)) {
            oldValue.value = value
            return true
        } else {
            return Reflect.set(target, key, value, receiver)
        }
    }
}

另外为了保证代码的健壮性,我们在 proxyRefs 入口处新增一个判断,若传入的对象属于响应式对象,则直接返回:

export function proxyRefs(objectWithRefs) {
    return isReactive(objectWithRefs)  // 新增判断
        ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

现在我们可以使用 proxyRefs 接口来快捷操作含有 ref 实例属性的对象了:

<body>
  <div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
  const { ref, proxyRefs, effect } = Vue;
  const div = document.querySelector('div');
  const info = ref('Hello');
  const obj = proxyRefs({ info });
  
  effect(() => {
    div.innerText = obj.info
  });
  
  obj.info = 'Bye~'; 
</script>

💡 Vue 官方并不推荐同时使用 Proxy 和 getter / setter 的能力,所以 proxyRefs 接口未收录在对外的官方文档中。