读 Vue3 issues - readonly() breaks comparison between objects #4986

原issues github.com/vuejs/vue-n…

共读:github.com/cuixiaorui/…

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版本来复现问题

image.png

因为仅仅涉及到js的,所以选择js的模版就好了,添加vue选择3.2.21版本

image.png

代码沙箱: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改变之后,打印一下:

image.png

对比,找到了不全等的原因,经过改变赋值,moreComplex.aReadonly 类型变成了 Reactive 类型

为什么 moreComplex.a = simpler.a 之后,就从 readonly 变成了 reactive 了?

看下 reactivereadonly 的逻辑

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.akeya ,对象是 moreComplex ,而 moreComplexreactive 对象 ,所以 moreComplex.a 自然就被转成 reactive 对象了

所以后面的全等对比 自然就是 false

解决问题

moreComplex.a 变成了普通的 value,所以才会被转换成 reactive

所以我们只需要在 set 的时候 检测 如果是 readonly 对象的话

那么就别在调用 toRaw 了 那 moreComplex.a 自然就变不成普通的 value

解决方案:

image.png

补上尤大的单元测试:

// #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
复制代码

image.png

值得学习之处:

  • 在对应 ut 上给个 issue 的编号
  • 处理 issue 的时候的标签管理,如优先级、类型等

基础知识

Vue3 响应性 API - v3.cn.vuejs.org/api/basic-r…

分类:
前端
标签: