4. 原始值的响应式方案
4. 原始值的响应式方案
霍春阳《Vue.js 设计与实现》的笔记
原始值指的是 Boolean
、Number
、 BigInt
、String
、Symbol
、undefined
和 null
等类型的值。在 JavaScript 中,原始值是按值传递的,而非按引用传递。这意味着,如果一个函数接收原始值作为参数,那么形参与实参之间没有引用关系,它们是两个完全独立的值,对形参的修改不会影响实参。
1. 引入 ref
Proxy 的代理目标必须是非原始值,所以为了实现原始值的响应式我们可以使用一个非原始值 去“ 包裹 ”原始值。
const wrapper = {
value: 'vue',
};
// 可以使用 Proxy 代理 wrapper,间接实现对原始值的拦截
const name = reactive(wrapper);
name.value; // vue
// 修改值可以触发响应
name.value = 'vue3';
为了方便和规范用户的使用,封装一个 函数 ,将包裹对象的创建工作都封装到该函数中。
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val,
};
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
});
// 将包裹对象变成响应式数据
return reactive(wrapper);
}
其中高亮部分为包裹对象定义了一个不可枚举且不可写的属性 __v_isRef
,用来判断一个数据是否是 ref。
2. 响应丢失问题
在 vue3 中,我们经常遇到一种情况:我们使用 reactive 创建了一个响应式的对象,我们在模板中使用这个对象中的某些属性,我们不希望在模板中频繁使用obj.prop
,这时便会用到解构赋值。
export default {
setup() {
// 响应式数据
const obj = reactive({ foo: 1, bar: 2 });
// 1s 后修改响应式数据的值,不会触发重新渲染
setTimeout(() => {
obj.foo = 100;
}, 1000);
return {
...obj,
};
},
};
上面的过程可以描述为:创建一个响应式的数据对象 obj,然后使用展开运算符得到一个新的对象 newObj,它是一个普通对象,不具有响应能力。
使用数据代理的方式,当访问
newObj.prop
时,实际访问的是obj.prop
toref 函数
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key];
},
};
// 定义 __v_isRef 属性
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
});
return wrapper;
}
toRefs 函数
function toRefs(obj) {
const ret = {};
// 使用 for...in 循环遍历对象
for (const key in obj) {
// 逐个调用 toRef 完成转换
ret[key] = toRef(obj, key);
}
return ret;
}
使用
const newObj = { ...toRefs(obj) };
toRefs
返回的对象结构
为
toRef
添加getter
toRef 函数
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key];
},
// 允许设置值
set value(val) {
obj[key] = val;
},
};
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
});
return wrapper;
}
3. 自动脱 ref
toRefs
会把响应式数据的第一层属性值转换为 ref
,因此必须通过 value
属性访问值,所以我们如果使用上面方式 return 数据的话,我们确实不需要频繁obj.prop
,而是变成了频繁prop.value
,所以需要自动脱 ref,即 如果读取的属性是一个 ref,则直接将该 ref 对应 的 value 属性值返回 。
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
// 自动脱 ref 实现:如果读取的值是 ref,则返回它的 value 属性值
return value.__v_isRef ? value.value : value;
},
});
}
// 调用 proxyRefs 函数创建代理
const newObj = proxyRefs({ ...toRefs(obj) });
同时也应该有自动为 ref 设置值的能力,为此添加 set 的拦截函数。
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
return value.__v_isRef ? value.value : value;
},
set(target, key, newValue, receiver) {
// 通过 target 读取真实值
const value = target[key];
// 如果值是 Ref,则设置其对应的 value 属性值
if (value.__v_isRef) {
value.value = newValue;
return true;
}
return Reflect.set(target, key, newValue, receiver);
},
});
}
我们在编写 Vue.js 组件时,组件中的 setup 函数所返回的数据会传递给 proxyRefs 函数进行处理 ,所以我们在模板中可以直接访问 ref 的值而不用.value
。
const MyComponent = {
setup() {
const count = ref(0);
// 返回的这个对象会传递给 proxyRefs
return { count };
},
};