原issues github.com/vuejs/vue-n…
issues 问题
Version:vue v3.2.21
Problem
I had an object A that used some data from a readonly object B, but after reassigning the props from object A and try to compare the prop to the equivalent data from object B, the result of the comparison was false when I expected to be true.
I managed to work around this by comparing the internal properties from both objects, but I'd like to know what it's expected to return in the case bellow
Steps to reproduce
class Type {
constructor(code) {
this.code = code;
}
getCode() {
return this.code;
}
}
simpler = readonly({ a: new Type(0), b: new Type(1) });
moreComplex = reactive({ a: simpler.a, b: simpler.b });
console.log(`Before: ${moreComplex.a === simpler.a}`);
moreComplex.a = simpler.b;
moreComplex.a = simpler.a;
console.log(`After: ${moreComplex.a === simpler.a}`);
复制代码
复现 issue
可以用 codesandbox.io ,方便选择vue版本来复现问题
因为仅仅涉及到js的,所以选择js的模版就好了,添加vue
选择3.2.21
版本
代码沙箱:codesandbox.io/s/vue-next-…
分析学习
源码单元测试
克隆最新的vue-next
并安装依赖
$ git clone https://github.com/vuejs/vue-next.git
$ cd vue-next
$ pnpm install
复制代码
切换到对应版本
$ git reset --hard v3.2.21 //issues 中 vue 版本
$ git reset --hard ORIG_HEAD //目前版本
复制代码
修改单元测试文件packages/reactivity/__tests__/reactive.spec.ts
:
//add
test.only('4896', () => {
class Type {
code:number
constructor(code:number) {
this.code = code;
}
getCode() {
return this.code;
}
}
let simpler = readonly({ a: new Type(0), b: new Type(1) });
let moreComplex = reactive({ a: simpler.a, b: simpler.b });
console.log(`Before: ${moreComplex.a === simpler.a}`);
moreComplex.a = simpler.b;
moreComplex.a = simpler.a;
console.log(`After: ${moreComplex.a === simpler.a}`);
})
复制代码
执行命令
$ pnpm run test reactive
复制代码
测试结果
分析问题
赋值 Before:moreComplex.a === simpler.a
结果为 true
后面改变了 moreComplex.a 的值,然后又改变回来
赋值 After:moreComplex.a === simpler.a
结果为 false
为什么两次结果不一样?为什么不全等了呢?
在moreComplex.a
改变之前,打印一下:
在moreComplex.a
改变之后,打印一下:
对比,找到了不全等的原因,经过改变赋值,moreComplex.a
由 Readonly
类型变成了 Reactive
类型
为什么 moreComplex.a = simpler.a 之后,就从 readonly 变成了 reactive 了?
看下 reactive
和 readonly
的逻辑
readonly
递归将所有对象属性设置为只读,也就是不能更改,没有set
操作,并且提供isReadonly=true
标识
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true, // 只读标识
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
);
}
复制代码
reactive
将全部设置响应式对象,提供 isReadonly=false
标识,target 如果是一个对象的话,会递归的调用 reactive 都给转换成 reactive,如果 target 值本身是 readonly 的话就不会再做处理
export function reactive(target: object) {
// if trying to observe a readonly proxy,
// return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target;
}
return createReactiveObject(
target,
false, // 可操作性标识
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
);
}
复制代码
set 赋值的逻辑
当赋值的时候,会触发 set
逻辑:
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key];
if (!shallow) {
value = toRaw(value); // moreComplex.a 从 readonly 转成普通对象
oldValue = toRaw(oldValue);
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
}
// ...
};
}
复制代码
对比的时候会首先触发 get
,而触发 get
的时候又会触发检测 value
是不是对象,如果是对象的话会继续转换,这里的转换取决于调用这个 key
的对象是什么类型,我们调用的是 moreComplex.a
的key
是 a
,对象是 moreComplex
,而 moreComplex
是 reactive
对象 ,所以 moreComplex.a
自然就被转成 reactive
对象了
所以后面的全等对比 自然就是 false
了
解决问题
moreComplex.a
变成了普通的 value
,所以才会被转换成 reactive
所以我们只需要在 set
的时候 检测 如果是 readonly
对象的话
那么就别在调用 toRaw
了 那 moreComplex.a
自然就变不成普通的 value
解决方案:
补上尤大的单元测试:
// #4986
test('setting a readonly object as a property of a reactive object should retain readonly proxy', () => {
const r = readonly({})
const rr = reactive({}) as any
rr.foo = r
expect(rr.foo).toBe(r)
expect(isReadonly(rr.foo)).toBe(true)
})
复制代码
这个测试简化了 issue
里面的情况,简化之后的问题就是,当给一个 reactive
对象 的 key
赋值为 readonly
的话,那么这个 key
应该还保持为 readonly
类型
最后跑下所有的测试,如果没有问题,证明这个 bug
修复完成
$ pnpm run test reactivity
复制代码
值得学习之处:
- 在对应 ut 上给个 issue 的编号
- 处理 issue 的时候的标签管理,如优先级、类型等
基础知识
Vue3 响应性 API - v3.cn.vuejs.org/api/basic-r…