6. 原始值的响应式方案
6.1 引入 ref 的概念
由于 Proxy
的代理目标必须是非原始值,所以需要使用一个非原始值去“包裹”原始值
js
const wrapper = {
value: "vue",
};
// 可以使用 Proxy 代理 wrapper,间接实现对原始值的拦截
const name = reactive(wrapper);
name.value; // vue
name.value = "vue3";
封装一个函数
js
// 封装一个 ref 函数
function ref(val) {
// 在 ref 函数内部创建包裹对象
const wrapper = {
value: val,
};
// 将包裹对象变成响应式数据
return reactive(wrapper);
}
但是为了让它有自动脱 ref
的能力,我们有必要区分一个数据到底是不是 ref
js
function ref(val) {
const wrapper = {
value: val,
};
// 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
Object.defineProperty(wrapper, "__v_isRef", {
value: true,
});
return reactive(wrapper);
}
6.2 响应丢失问题
通过解构对象的方式会使模板数据丢失响应式
js
export default {
setup() {
// 响应式数据
const obj = reactive({ foo: 1, bar: 2 });
// 将数据暴露到模板中
return {
...obj,
};
// 相当于
// return {
// foo: 1,
// bar: 2,
// };
},
};
如何解决?
js
// obj 是响应式数据
const obj = reactive({ foo: 1, bar: 2 });
// newObj 对象下具有与 obj 对象同名的属性,并且每个属性值都是一个对象,
// 该对象具有一个访问器属性 value,当读取 value 的值时,其实读取的是 obj 对象下相应的属性值
const newObj = {
foo: {
get value() {
return obj.foo;
},
},
bar: {
get value() {
return obj.bar;
},
},
};
effect(() => {
// 在副作用函数内通过新的对象 newObj 读取 foo 属性值
console.log(newObj.foo.value);
});
// 这时能够触发响应了
obj.foo = 100;
将 foo
和 bar
方法封装成一个toRef
方法
js
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key];
},
};
return wrapper;
}
// 使用
return {
foo: toRef(obj, "foo"),
bar: toRef(obj, "bar"),
};
继续优化,批量解构
js
function toRefs(obj) {
const ret = {};
// 使用 for...in 循环遍历对象
for (const key in obj) {
// 逐个调用 toRef 完成转换
ret[key] = toRef(obj, key);
}
return ret;
}
// 使用
return {
...toRefs(obj),
};
完整 toRef
方法
js
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key];
},
// 允许设置值
set value(val) {
obj[key] = val;
},
};
// 将通过 toRef 或 toRefs 转换后得到的结果视为真正的 ref 数据
Object.defineProperty(wrapper, "__v_isRef", {
value: true,
});
return wrapper;
}
6.3 自动脱 ref
js
const obj = reactive({ foo: 1, bar: 2 });
obj.foo; // 1
obj.bar; // 2
const newObj = { ...toRefs(obj) };
// 必须使用 value 访问值
newObj.foo.value; // 1
newObj.bar.value; // 2
使用 Proxy
为 newObj
创建一个代理对象,通过代理来实现脱 ref
js
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) });
实际上,我们在编写 Vue.js 组件时,组件中的 setup
函数所返回的数据会传递给 proxyRefs
函数进行处理:
js
const MyComponent = {
setup() {
const count = ref(0);
// 返回的这个对象会传递给 proxyRefs
return { count };
},
};
为 proxyRefs
添加设置值的能力
js
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);
},
});
}