前言
官方文档中对reactive
的描述:
响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。
官方文档中对readonly
的描述:
只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。
这意味着嵌套对象内的对象拥有和原对象一样的功能。
简单的来实践测试一下:
<script setup>
import { isReactive, isReadonly, reactive, readonly } from "vue";
const original = reactive({
nested: {
foo: 1,
},
array: [{ bar: 2 }],
});
const copy = readonly(original);
</script>
<template>
{{ isReactive(original.nested) }}
{{ isReactive(original.array) }}
{{ isReactive(original.array[0]) }}
{{ isReadonly(copy.nested) }}
</template>
页面中显示四个true
。
那测试用例可以完善一下,测试之前实现的代码是否支持这样的功能。
reactive
在reactive.spec.ts
中添加测试用例:
it("nested reactive", () => {
const original = reactive({
nested: { foo: 1 },
array: [{ bar: 2 }],
});
expect(isReacive(original.nested)).toBe(true);
expect(isReacive(original.array)).toBe(true);
expect(isReacive(original.array[0])).toBe(true);
});
执行单测,如期不通过。
实现
实现思路,访问嵌套对象内对象时候,将内部对象也实现响应式,即调用reactive
方法。那前提是访问的是对象,则需要添加一个是否是对象的判断。
import { isObject } from "../shared";
function createGetter(isReadonly = false) {
return function get(target, key) {
let res = Reflect.get(target, key);
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
}
if (isObject(res)) {
return reactive(res);
}
if (!isReadonly) {
track(target, key);
}
return res;
};
}
在公共方法中导出isObject
方法,
export function isObject(val) {
return val !== null && typeof val === "object";
}
执行单测yarn test reactive
readonly
在readonly.spec.ts
中完善对嵌套对象的断言,
it("happy path", () => {
const original = { foo: 1, bar: { baz: 2 } };
const wapper = readonly(original);
expect(wapper).not.toBe(original);
expect(wapper.foo).toBe(1);
expect(isReadonly(wapper)).toBe(true);
expect(isReadonly(original)).toBe(false);
// 判断嵌套对象
expect(isReadonly(wapper.bar)).toBe(true);
});
执行单测yarn test readonly
,预期不通过。
实现
readonly
的实现和reactive
一致。
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
执行单测,
最后再执行全部测试用例,验证是否对其他功能存在影响。
shallowReadonly
如果你在上文中点进了官方文档的链接,在readonly
的详细描述中:
要避免深层级的转换行为,请使用 shallowReadonly() 作替代。
和readonly()
不同,shallowReadonly
没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为ref
的属性不会被自动解包了。
单测
新增shallowReadonly.spec.ts
。
shallowReadonly
是浅层转换,还是用isReadonly
进行检测,最外层的是可读的,深层次的不是。
it("should not make non-reactive properties reactive", () => {
const original = reactive({ foo: 1, bar: { baz: 2 } });
const wapper = shallowReadonly(original);
expect(isReadonly(wapper)).toBe(true);
expect(isReadonly(wapper.bar)).toBe(false);
});
实现
在reactive.ts
导出shallowReadonly
方法,和readonly
类似,只是handler
参数不同,
export function shallowReadonly(raw) {
return createActiveObject(raw, shallowReadonlyHandler);
}
在baseHandler.ts
导出shallowReadonlyHandler
对象,
export const shallowReadonlyHandler = {}
handler
中get
操作都是根据createGetter
方法生成,那shallowReadnoly
的特点是最外层的是可读的,深层次的不是,可以像readonly
一样添加一个参数来判断,如果是shallow
直接返回结果,也不需要进行深层次的转换和track
。
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key) {
let res = Reflect.get(target, key);
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
}
if (shallow) {
return res;
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
if (!isReadonly) {
track(target, key);
}
return res;
};
}
全局定义:
const shallowReadonlyGet = createGetter(true, true);
shallowReadonlyHandler
对象和readonlyHandler
相似,只是get
不同,可以复用之前实现的extend
方法。
export const shallowReadonlyHandler = extend({}, readonlyHandler, {
get: shallowReadonlyGet,
});
那可以顺便验证shallowReadonly
的更新操作的测试用例,
it("should call console warn when call set", () => {
console.warn = jest.fn();
const original = shallowReadonly({ foo: 1 });
original.foo = 2;
expect(console.warn).toHaveBeenCalled();
});
执行单测yarn test shallowReadonly
总结
reactive
响应式转换是“深层”的:它会影响到所有嵌套的属性。readonly
只读代理是深层的,对任何嵌套属性的访问都将是只读的。- 获取对象深层次嵌套,会触发
get
操作,在判断该嵌套目标也是对象类型时就可以再次用reactive
或readonly
包裹返回。 shallowReadonly
就是在深层次转换和track
逻辑之前返回结果即可。