本文为《Vue.js设计与实现》的笔记。
1. 原始值
原始值是指 Boolean Number BigInt String Symbol undefined null 等类型的值。
2. ref
ES6中的Proxy仅可代理非原始值,而对原始值的操作无法拦截。
问题解决思路:使用一个非原始值去“包裹”原始值,再将包裹对象变成响应式数据。
const wrapper = {
value: 'vue'
};
const name = reactive(wrapper);
其中reactive为将非原始值变成响应式对象的方法,内部使用Proxy实现。
上述代码由两个问题:
- 不友好:用户为了创建一个响应式的原始值,不得不创建一个包裹对象
- 不规范:随意命名,可能不使用value,使用val等
为解决以上问题,提供一个封装函数,统一行为:
function ref(val){
// 创建包裹对象
const wrapper = {
value: val
}
// 将包裹对象变成响应式数据
return reactive(wrapper);
}
具体使用:
const refVal = ref(1);
effect(() => console.log(refVal.value));
refVal.value = 2;
3. 区分ref与reactive
在模版中ref的数据,我们需要自动进行脱ref的操作,这就涉及到如何区分一个数据是不是ref,而上面ref的实现无法做到这样的区分。
修改ref:
function ref(val) {
// 创建包裹对象
const wrapper = {
value: val,
};
// 在wrapper上定义一个不可枚举的属性
Object.defineProperty(wrapper, "__v_isRef", {
value: true,
});
// 将包裹对象变成响应式数据
return reactive(wrapper);
}
上面的代码使用Object.defineProperty在wrapper对象上定义一个不可枚举的属性__v_isRef,其值为true。可通过这一属性来判断一个数据是否为ref。
4. 用于解决响应丢失问题
先看看什么是响应丢失问题:
export default {
setup(){
const state = reactive({foo: 1, bar: 2})
return { ...state }
}
}
此处使用了展开运算符将响应式对象暴露到外部,使数据丢失了响应式
return { ...state }
// 等价于
return {foo: 1, bar: 2}
其实就是返回了一个普通对象,不具有响应式能力。
前面讲了我们会在模版中访问ref时会进行自动脱ref功能,换句话说,此处return的数据的键对应的值为一个ref是可以接受的(提供ref作为可使用数据是可接受的)
即state中为如此形式:
export default {
setup(){
const state = reactive({foo: ref(1), bar: ref(2)})
return { ...state }
}
}
这样相当于:
return { ...state }
// 等价于
return {foo: ref(1), bar: ref(2)}
这样展开运算符便不会使数据丢失响应式。但这样的形式十分繁琐,为非原始值每一个值套一个ref,是的state在setup中的使用也不方便。不修改原有reactive的数据,而是间接地获取数据:
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key];
},
set value(val) {
obj[key] = val;
},
};
// 在wrapper上定义一个不可枚举的属性
Object.defineProperty(wrapper, "__v_isRef", {
value: true,
});
return wrapper;
}
定义了一个toRef函数,入参obj为一个响应式数据,key为该对象的一个键。该函数内部使用一个wrapper对象,使用一个访问器属性value,当读取value的值时,最终读取的是obj的key的值,设置value的值时,最终设置的是响应式数据的同名属性的值。
toRef只能对单个key进行处理,我们定义一个toRefs来对一个响应式对象批量地进行转换:
function toRefs(obj) {
const ret = {};
// 使用 for...in... 遍历对象
for (const key in obj) {
// 逐个调用toRef完成转换
ret[key] = toRef(obj, key);
}
return ret;
}
对本小节响应丢失的情况,使用toRefs即可解决:
export default {
setup(){
const state = reactive({foo: 1, bar: 2})
return { ...toRefs(state) }
}
}
4. 自动脱ref
4.1 toRefs带来的问题
toRefs函数解决了响应式丢失的问题,但也带来了新的问题,那就是它会把响应式数据的第一层属性值转换为ref,意味着必须要通过value属性去访问值,这增加了用户的心智负担,因此,需要提供自动脱ref的能力。
4.2 自动脱ref实现
// 自动脱ref
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;
},
set(target, key, newValue, receiver) {
// 通过target[key]访问真实值
const value = target[key];
if (value.__v_isRef) {
value.value = newValue;
return true;
}
return Reflect.set(target, key, newValue, receiver);
},
});
}
proxyRefs为target创建一个代理对象,主要根据__v_isRef来判断当前数据是否为ref,在进行读取操作时,若取得的值为ref,则自动返回其value的值,在进行设置操作时,若键对应的原值为ref,则设置其value的值。
实际上,Vue组件中的setup函数所返回的数据会传递给proxyRefs进行处理
const setupResult = setup(shallowReadonly(instance.props), {
emit: instance.emit,
});
instance.setupState = proxyRefs(setupResult);