VueUse 源码解析之 toRefs

525 阅读4分钟

VueUse  源码解析之 toRefs

VueUse 源码解析之 toRefs

TL;DR

扩展 Vue3 的 toRefs API。

学前知识

  1. toRef()
  2. toRefs()
  3. reactive

toRef 处理 reactive 无法解构

一般来讲复杂数据类型,我们都是通过 reactive 来管理的,就像下面这样。

<script setup>
	import { reactive } from "vue";
	const user = reactive({ name: "Vue3", age: 1 });
</script>

当数据结构复杂,我们在读取数据的时候链式打点很不方便和直观,比如:

const user = reactive({
	data: {
		name: "Vue3",
		age: 1,
		hobby: ["basketball", "football", "music", "dance"]
	}
});

const name = user.data.name 返回字符串 Vue3。除了链式打点赋值 还可以使用解构赋值const { name } = user.data

但是无论是解构赋值还是链式打点赋值,新定义的变量都没有响应特性,原因很好理解,因为没有对新创建的变量进行响应式代理。

接下来我们很自然想到通过 ref API 对想要获取的值进行响应式代理:


const name = ref(user.data.name);

// or
const { name } = user.data;
const newName = ref(name);

上面这种情况值得说说:

  1. 假如 ref 的是基本数据类型,基本类型赋值给变量,会创建一个新的地址存储值。源数据和新数据完全没关系了。此时修改 reactive 的值是无法自动关联到新的变量,修改新的变量源数据也不会自动响应,因为变量就是一个纯粹的值,基本类型情况下丧失了数据的关联性
  2. 假如 ref 的复杂数据类型,复杂类型赋值给变量,并不会创建一个新的地址,而是新老变量都指向同一个地址,所以新老数据彼此是关联的,比如下面例子:
<script setup>
import { reactive, ref } from "vue";
const user = reactive({
	data: {
		name: "Vue3",
		age: 1,
		hobby: ["basketball", "football", "music", "dance"]
	}
});
	const refDance = ref(user.data.hobby);
</script>

<template>
	<h1>ref dance: {{ refDance[3] }}</h1>
	<h1>reactive dance: {{ user.data.hobby[3] }}</h1>
	<label>
		change reactive dance:
		<input type="text" v-model="user.data.hobby[3]" />
	</label>
	<label>
		change ref dance:
		<input type="text" v-model="refDance[3]" />
	</label>
</template>

可见使用 ref 给值设置响应式特性并不是理想的解决办法,那怎么解决呢,你想,可以对地址代理,地址所在地不就是位置吗,这思路不就来了,不对读取值进行代理,而是直接代理 reactive 给的 key 的所在位置,即给一个位置创建响应式特性。

import { isRef } from "vue";

class ObjectRefImpl {
	#object;
	#key;

	constructor(_object, _key) {
		this.#object = _object;
		this.#key = _key;
	}

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

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

function toRef(object, key,) {
	const val = object[key];
	// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
	return isRef(val) ? val : new ObjectRefImpl(object, key);
}

ref 对值进行代理,toRef 对位置进行代理,因为这个区别,这也就是为什么 ref 使用 RefImpl 类实现 toRef 使用 ObjectRefImpl 类实现。

我们测试下能不能运行,案例代码如下:

<script setup>
import { reactive, isRef } from "vue";

class ObjectRefImpl {
	#object;
	#key;

	constructor(_object, _key) {
		this.#object = _object;
		this.#key = _key;
	}

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

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

function toRef(object, key,) {
	const val = object[key];
	// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
	return isRef(val) ? val : new ObjectRefImpl(object, key);
}

const user = reactive({
	name: "Vue3",
	age: 1,
	hobby: ["basketball", "football", "music", "dance"]
});
const name = toRef(user, "name");
</script>

<template>
	<h1>ref name: {{ name.value }}</h1>
	<h1>reactive name: {{ user.name }}</h1>
	<label>
		change reactive name:
		<input type="text" v-model="user.name" />
	</label>
	<label>
		change ref name:
		<input type="text" v-model="name.value" />
	</label>
</template>

不用想了,效果非常好,其实 Vue3 的 toRef() API 就干了这么一件事,查看 toRef 源码,发现函数还有第三个参数增加了默认值,这点逻辑难度对你肯定不难。

export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]> {
  const val = object[key]
  return isRef(val)
    ? val
    : (new ObjectRefImpl(object, key, defaultValue) as any)
}

下面是 ObjectRefImpl 类的实现:

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K]
  ) {}

  get value() {
    const val = this._object[this._key]
    return val === undefined ? (this._defaultValue as T[K]) : val
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

通往罗马的路不止一条

对 reactive 的 key 进行位置代理我们实现了一个 ObjectRefImpl 类,除了自己实现还可以利用 Vue3 提供的 computed 的 get 和 set 实现。

function toRef(object, key,) {
	const val = object[key];
	// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
	return isRef(val) ? val : computed({
		get: () => {
			return object[key];
		},
		set: (newValue) => {
			object[key] = newValue;
		}
	});
}

VueUse 扩展 toRefs 选择的就是这条路。

同样运行上面的案例测试:

<script setup>
import { reactive, isRef, computed } from "vue";

function toRef(object, key,) {
	const val = object[key];
	// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
	return isRef(val) ? val : computed({
		get: () => {
			return object[key];
		},
		set: (newValue) => {
			object[key] = newValue;
		}
	});
}

const user = reactive({
	name: "Vue3",
	age: 1,
	hobby: ["basketball", "football", "music", "dance"]
});
const name = toRef(user, "name");
console.log(name);
</script>

<template>
	<h1>ref name: {{ name }}</h1>
	<h1>reactive name: {{ user.name }}</h1>
	<label>
		change reactive name:
		<input type="text" v-model="user.name" />
	</label>
	<label>
		change ref name:
		<input type="text" v-model="name" />
	</label>
</template>

toRefs 批量解构 reactive

如果我们批量解构 reactive toRef() 就无法胜任了,借用 toRef() 的代码,使用一个 for 循环就实现了 toRefs()

export function toRefs(object) {
  if (!isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

现在来看 toRefs() 的概念是不是好理解了。

toRefs():将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

扩展 toRefs 的功能

支持 ref

我们知道当 ref API 接收一个复杂类型,内部自动调用 reactive 创建响应式。

<script setup>
import { ref, toRefs } from "vue";

const refObj = ref({
	name: "VueUse",
	age: 18
});

console.log(refObj.value);

</script>

现在你的链式打点又多了一层 .value,为了支持 ref 复杂数据的提取 toRefs 需要简单的扩展对 ref 复杂数据的支持,只需要增加一个判断就行了。

export function toRefs(maybeRefobject) {
	const object = isRef(maybeRefobject) ? maybeRefobject.value : maybeRefobject;
	if (!isProxy(object)) {
		console.warn(`toRefs() expects a reactive object but received a plain one.`)
	}
	const ret = Array.isArray(object) ? new Array(object.length) : {}
	for (const key in object) {
		ret[key] = toRef(object, key)
	}
	return ret
}