一、前言
先抛几个问题:
ref
必须通过.value
的方式访问?- Vue 3 中解构不能随意使用?比如
props
原始值解构(可能会)或reactive()
解构等存在响应性连接丢失场景? reactive()
有哪些响应性连接丢失场景?Vue3 禁止解构reactive()
?
二、为什么 ref 必须通过 .value 的方式访问
来看看 ref
具体实现:
import { reactive } from './reactive'
import { trackEffects, triggerEffects } from './effect'
export const isObject = (value) => {
return typeof value === 'object' && value !== null;
}
// 将对象转换为响应式
function toReactive(value) {
// reactive() 对对象中深层嵌套对象同样会进行代理
return isObject(value) ? reactive(value) : value;
}
class RefImpl {
public _value;
public dep = new Set; // 依赖收集
public __v_isRef = true; // ref 标识
constructor(public rawValue, public _shallow) {
// rawValue 传递进来的值
// _shallow 浅 ref 表示不需要再次代理
this._value = _shallow ? rawValue : toReactive(rawValue);
}
get value() {
trackEffects(this.dep); // 取值的时候依赖收集
return this._value;
}
set value(newValue) {
if(newValue != this.rawValue) {
this._value = this._shallow ? newValue : toReactive(newValue);
this.rawValue = newVal;
triggerEffects(this.dep);
}
}
}
上述代码中,可以发现对于原始值默认会包装为一个对象 toReactive(rawValue)
,然后通过 get value()
和 set value()
属性访问器(Accessor Properties)特性定义原始值的访问和设置行为,从而导致必须有 .value
的操作。这也解答了第一个问题。
这里要先知道一点,vue3 中,通过 getter / setters 来实现 ref,通过 Proxy 来实现 reactive。
注意,在 ref
实现里我们还发现另外两个额外的特性:
- _shallow
_shallow
应用于如 shallowRef()
。
浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。因为我们通过源码知道,ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map,ref 会使它们的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到。
- isObject
isObject(value) ? reactive(value) : value;
import { ref } from 'vue'
const obj = { a: 1, b: { c: 2 } };
const count = ref(obj)
function change1() {
count.value.b = { c: 3 };
}
function change2() {
count.value.b.c = 4;
}
上述两个 change 方法都会触发视图更新。说明如果传给 ref 的是一个对象,它会将其进行 reactive()
封装,我们知道 reactive 是一个深层嵌套响应式实现。
三、解构响应性连接丢失场景
1. Proxy Intercepting
先看下述代码:
const obj = {
name: 'foo',
}
const handler = {
get: function(target, key) {
console.log('get', key);
return Reflect.get(...arguments);
},
set: function(target, key, value) {
console.log('set', key, '=', value);
return Reflect.set(...arguments);
},
}
const data = new Proxy(obj, handler);
data.name = 'foo2';
console.log(data.name);
// set name = foo2
// get name
// foo2
上述代码中,我们发现,proxy 的使用本身就是对于对象的拦截。
2. reactive()
首先,对于普通对象,如果我们在对象内嵌套一层对象,然后进行 proxy 代理,是否能拦截这个嵌套的对象?
答案是不能。
const obj = {
count: 1,
b: {
c: 2
}
};
const handler = {
get: function(target, key) {
console.log('get', key);
return Reflect.get(...arguments);
},
set: function(target, key, value) {
console.log('set ', key, '=', value);
return Reflect.set(...arguments);
},
}
const data = new Proxy(obj, handler);
console.log(data.count);
console.log(data.b)
console.log(data.b.c)
// get count
// 1
// get b
// {c: 2}
// get b
// 2
可以发现访问 data.b.c
没有触发对象 c 的 get 执行,只 b 对象执行了 get,即无法拦截。
因此如果想要对深层嵌套的对象实现响应性,需要对嵌套的对象也进行一层 proxy 代理,在 vue3 中 reactive()
其实就是一个深层嵌套响应式实现。
如下是 reactive()
简单实现:
const data = reactive(obj)
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
console.log("get");
if(typeof target[key] === 'object') {
return reactive(target[key]); // 递归去进行 proxy 绑定
}
return Reflect.get(target, key, receiver); // Reflect.get(...arguments);
},
set(target, key, value, receiver) {
console.log("set");
return Reflect.set(target, key, value, receiver); // Reflect.set(...arguments);
}
})
}
明白了 reactive()
简单的实现,回到标题,如果我们进行解构,会发生什么?
const obj = {
a: {
count: 1
},
b: 1
}
const proxy = reactive(obj);
const { a, b } = proxy;
console.log('---')
console.log(a);
console.log(b);
console.log(a.count);
// get
// get
// ---
// ---console.log(a);
// Proxy(Object) {count: 1}
// 1
// ---console.log(a.count);
// get
// 1
可以发现解构之后,console.log(b);
获取不到 obj.b
,a
以及 a.count
均可以正常访问。这是为什么?
需要先学习一下解构赋值原理。解构赋值区分:
- 原始类型的赋值:按值传递
- 引用类型的赋值:按引用传递
const a = {
b: 1
}
const c = a.b;
上述代码,访问 c 相当于直接访问这个值,绕过了 a 的 proxy 也就失去了响应性连接。
const a={
b: {
d: 3
}
}
const c = a.b;
访问 c 相当于直接访问 b 的代理对象,因此并没有失去响应性连接。
因此,Vue3 推荐索性不用解构(毕竟有时候会出现响应性连接丢失,开发者埋怨怎么 Vue 有问题,其实不然)。
3. props.x
同样的,props 不推荐解构。
如果想要解构,可以使用 toRefs
将 props
解构为响应式的引用,以确保在解构后的属性上保持响应式绑定。
toRefs
函数将 props 对象中的每个属性都转换为一个独立的响应式引用,并返回一个新的对象,该对象包含了这些响应式引用。
以下是一个示例,演示了如何在 Vue 3 中使用 toRefs
来解构 props 并保持响应式绑定:
<template>
<div>
<p>{{ name }}</p>
<p>{{ age }}</p>
</div>
</template>
<script setup props="props">
import { toRefs } from 'vue';
const { name, age } = toRefs(props);
</script>
需要注意的是,toRefs
只会在组件初始化时执行一次,所以解构后的属性仅在组件初始化时保持响应式绑定。什么意思?即解构后得到的属性,如果在组件的生命周期内重新分配或修改,这些属性将不再保持与 props 对象的同步更新。
下面是 toRefs
函数的简单实现:
import { ref, isRef } from 'vue';
export function toRefs(obj) {
const ret = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
ret[key] = isRef(value) ? value : ref(value);
}
}
return ret;
}
上述代码首先遍历传入的响应式对象 obj
的每个属性,然后判断属性是否是 ref 对象,如果是,直接赋值,否则用 ref 封装原始值。最终返回一个普通对象 ret
,该对象的每个属性都是原始响应式对象中属性的引用。