Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

readonly() breaks comparison between objects #4986 #1

Open
cuixiaorui opened this issue Nov 30, 2021 · 11 comments
Open

readonly() breaks comparison between objects #4986 #1

cuixiaorui opened this issue Nov 30, 2021 · 11 comments

Comments

@cuixiaorui
Copy link
Owner

cuixiaorui commented Nov 30, 2021

readonly() breaks comparison between objects #4986

为什么要读他

  • 够简单
  • issue 里面描述 bug 的过程
  • 尤大修复 bug 的全过程

可以学到什么

  • vue3 中修复bug 的步骤
  • 如何描述你的bug
  • vue3 中 issues 标签的管理

视频讲解

【共读 Vue3】如何测试代码的执行速度

开始时间

2021-12-01

12月第一周

@fengjinlong
Copy link

投资学习是不会亏本的,为了心中的 M1 MAX 🌹

@oceanlvr
Copy link

oceanlvr commented Nov 30, 2021

我先来开个头,首先调试这个 issue 源码执行对应的步骤有两种方法(我能想到的

  1. 比较适用于所有项目包括无测试项目的,但是比较麻烦。调试依赖比如vue源码的时候 先是下载依赖源码 然后开启 sourcemap build 后的文件 link 到全局,然后去在要调试的项目中去 npm link 这个 build 文件。

目标,进入到依赖 vue 中查看对应的源码。

  • vue npm link 到 global
  • 项目 npm link 全局 vue
  • 项目中打断点
  • 调试 vue 源码和项目源码

具体方法参考若川巨神的文章 https://juejin.cn/post/6994976281053888519#heading-4

  1. 直接在依赖的测试文件中调试,适用于有测试文件的项目,方法是模仿之前已有的测试用例,复现这个 issue,然后直接打断点调试即可。

具体方法参考

@Cleam
Copy link

Cleam commented Nov 30, 2021

👍 Work hard, study harder.

@cuixiaorui cuixiaorui changed the title 【共读 issues】4986 【共读 issues】#4986 readonly() breaks comparison between objects Nov 30, 2021
@likui628
Copy link

likui628 commented Nov 30, 2021

测试代码

问题出现的原因将readonly对象r复制给rr.foo丢失了readonly属性 代码沙箱

import { reactive, readonly, isReadonly } from "vue"; //vue 3.2.21

const r = readonly({});
const rr = reactive({});
rr.foo = r;
console.log("equal: ", rr.foo === r);  //equal:  false
console.log("Before isReadonly:", isReadonly(r));//Before isReadonly:true
// 问题出在readonly属性
console.log("After isReadonly:", isReadonly(rr.foo)); //After isReadonly:false

vscode上调试

package/reactivity/__tests__/readonly.spec.ts
image
package.jsontest上悬浮并点击Debug Script进行调试
image

修复原理

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 && !isReadonly(value)) {
     //如果不是readonly对象,参数value通过toRaw重设为原始对象
      value = toRaw(value)
      oldValue = toRaw(oldValue)
    } 
    //如果是readonly对象,参数value保持不变
    const result = Reflect.set(target, key, value, receiver)
    return result
  }
}

闭包的运用,以isReadonly为例

创建readonly对象

const r = readonly({});

调用createReactiveObject,传入isReadonly为true,collectionHandlers为readonlyHandlers

export function readonly<T extends object>(
  target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true,//只读
    readonlyHandlers,//只读对象Handlers
    readonlyCollectionHandlers,//只读集合Handlers
    readonlyMap
  )
}

createReactiveObject返回一个新的Proxy对象

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  const proxy = new Proxy(
    target,
    baseHandlers
  )
  return proxy
}

代理对象get通过createGetter(true)返回一个get函数

export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
}

const readonlyGet = /*#__PURE__*/ createGetter(true)

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } 
}

isReadonly通过代理调用了get,返回了之前通过闭包保存的isReadonly值true

export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

@sundada88
Copy link

题目理解:当我的的reactive对象的属性值赋值readonly类型的值的时候,那么reactive对象的属性值应该保留readonly的proxy

问题诊断:可以打印出两个属性值的__v_isReadonly属性

    console.log("moreComplex.a", moreComplex.a.__v_isReadonly); // 3.2.21 => false, // 修复版本 => true
    console.log("simpler.a", simpler.a.__v_isReadonly); // 3.2.21 => true, // 修复版本 => true

出现问题的原因:在 3.2.21版本中,处理reactive的set的时候没有过滤value是readonly类型的情况,所以给value赋值了readonly数据的raw类型,代码如下

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) // 当readonly数据类型过来的时候,找到源数据进行赋值
      oldValue = toRaw(oldValue)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

问题修复:在修复版本,加上了给reactive数据类型赋值readonly类型的处理

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 && !isReadonly(value)) { // 对属性值是readonly类型的数据进行了过滤
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

@Magua-Q
Copy link

Magua-Q commented Dec 1, 2021

为了我的奥迪,为了媳妇儿的迪奥,开始卷,xdm,fighing!🌹

@GuanLola
Copy link

GuanLola commented Dec 1, 2021

版本:v3.2.21
if (!shallow)
结果:
3-2-21

版本:v3.2.23
if (!shallow && !isReadonly(value))
j结果:
3-2-23

@jp-liu
Copy link

jp-liu commented Dec 4, 2021

issues 标号 #4986

issues 描述: vuejs/core#4986

问题

class Type {
  constructor(code) {
    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}`);

这是为什么呢,通过反复赋值,就不相等了?
解析:

  1. reactive/readonly 通过 Proxy 将对象进行代理,设置get/set 来实现响应式系统,既然我们的值都是通过get返回的,那么问题肯定出现在 get 函数上,返回的值不一致
  2. 为什么会出现不一致呢? 可操作性对象和只读对象,在 vue3 响应式系统中是通过闭包提供的 isReadonly 来识别
  3. 出现了不一致,肯定是 isReadonly 标识导致,接下来看看代码

isReadonly

  • 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 标识

    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
      );
    }

一个对象拥有 isReadonly=true 的标识,标识不可操作和变更,而在 reactive 对象的 set 中,有一个操作,却改变了这个行为

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    /**
     * 在`set`函数中,只要响应式对象没有设置`shallow`标识(浅层),则会获取元值,用于比较,判断内部是否变更
     * 
     * 判断点
     *   - 第一个判断指定`shallow`浅层的话,不需要原值,直接比较两个值,还有一种情况
     *   - 第二个判断,如果对象值是 `readonly` 则不需要取出原值比较,因为不可能改变,`set`都没有,应该保持现状,不能动
     * 所以少了第二个判断点
     */
    let oldValue = (target as any)[key];
    if (!shallow) {
      // 这里出问题了,不应该取`readonly`对象的原值,导致它失去了只读,然后又因为是对象,所以,在访问的时候,会被重新 `reactive` 代理成为响应式对象,就可以变更了
      value = toRaw(value);
      oldValue = toRaw(oldValue);
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value;
        return true;
      }
    }
    // ...
  };
}

在看问题

  const { readonly, reactive } = Vue
  class Type {
    code
    constructor(code) {
      this.code = code
    }

    getCode() {
      return this.code
    }
  }
  
  let simpler = readonly({ onlyTypeA: new Type(0), onlyTypeB: new Type(1) })
  let moreComplex = reactive({ mutableA: simpler.onlyTypeA, mutableB: simpler.onlyTypeB })

  // 当前都是`readonly`对象
  //    -`reactive` 的 `get` 返回值 if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target }
  //    -`readonly` 的 `get` 返回值 if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) { return target }
  // 可见都是一样的,`readonly`的时候,都是返回当前 `proxy` 对象
  console.log(`Before: ${moreComplex.mutableA === simpler.onlyTypeA}`)

  // 调用`set`的时候,没有判断新值是不是只读对象,直接进行`toRaw`处理,取得了元对象
  // 然后会取出`onlyTypeB`,进行处理,丢失了`ReactiveFlags.IS_READONLY`
  // 导致后续`get`的值两个不相等
  moreComplex.mutableA = simpler.onlyTypeB
  moreComplex.mutableA = simpler.onlyTypeA // 和上面一样的原因
  console.log(`After: ${moreComplex.mutableA === simpler.onlyTypeA}`)

修改源码

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    // 增加一个判断,不是`readonly`才进行进入
    if (!shallow && !isReadonly(value)) {
     // 如果不是readonly对象,参数value通过toRaw重设为原始对象
      value = toRaw(value)
      oldValue = toRaw(oldValue)
    } 
    // 如果是readonly对象,参数value保持不变
    const result = Reflect.set(target, key, value, receiver)
    return result
  }
}

@cuixiaorui cuixiaorui changed the title 【共读 issues】#4986 readonly() breaks comparison between objects #4986 readonly() breaks comparison between objects Dec 6, 2021
@cuixiaorui cuixiaorui changed the title #4986 readonly() breaks comparison between objects readonly() breaks comparison between objects #4986 Dec 6, 2021
@qinran0423
Copy link

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)
  })
rr.foo = r

这一步赋值了一个readonly属性的值而丢失了readonly属性
问题在于:

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)
      oldValue = toRaw(oldValue)
    } 
    const result = Reflect.set(target, key, value, receiver)
    return result
  }
}

赋值出发了set

value = toRaw(value)

value通过toRaw重设为原始对象,所以要对readonly属性进行过滤

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 && !isReadonly(value)) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
    } 
   
    const result = Reflect.set(target, key, value, receiver)
    return result
  }
}

@Leiloloaa
Copy link

解决步骤

  • 断点调试 看看步骤的结果
  • 通过断点调试 发现 赋值的时候 改变了 类型
    let r = readonly({ a: new Type(0), b: new Type(1) })
    let rr = reactive({ a: r.a, b: r.b })
    console.log(`Before: ${rr.a === r.a}`) // true
    console.log(isReactive(rr.a)) // false
    console.log(isReadonly(rr.a)) // true
    rr.a = r.b
    console.log(isReactive(rr.a)) // true
    console.log(isReadonly(rr.a)) // false
    rr.a = r.a
    console.log(rr.a) // { code: 0 }
    console.log(r.a) // { code: 0 }
    console.log(`After: ${rr.a === r.a}`) // false
  • 找到 reactive 的 set 逻辑是在
    • 在这里的时候 value 被 toRaw 是做了处理获取到 .value 的值了,所以这里的 value 是发生改变了 就已经不在是之前的 readonly value 了
  • 搞清楚这个问题得先搞清除 reactive 和 readonly 的逻辑
    • reactive(value )
      • 这里的 value 如果是一个对象的话
      • 会递归的调用 reactive 都给转换成 reactive
    • Readonly
      • 如果 value 值本身是 readonly 的话 那么就不会在做处理了
    • 当赋值的时候
      会触发 set 逻辑
      而在 set 里面
      会有一个调用 toRaw 函数的动作
      所以这里就会把 .a 转换成 普通的值对象了(从 readonly 转成 普通对象)
  • 接着到这里的对比逻辑
    这里对比的时候会首先触发 get
    而触发 get 的时候 又会触发检测 value 是不是对象 如果是对象的话 他会继续 转换
    这里的转换取决于 调用这个 key 的对象是什么类型
    我们调用的是 rr.a key 是 a ,对象是 rr
    而 rr 是 reactive 对象
    所以 .a 自然就被转成 reactive 对象了

分析问题的话 是因为 .a 变成了普通的 value 所以才会被转换成 reactive 所以我们只需要在 set 的时候 检测 如果是 readonly 对象的话 那么就别在调用 toRaw 了 那 .a 自然就变不成普通的 value

解决方案

在 set 的时候,如果是 readonly 就不转换成普通变量

@runningOrange
Copy link

调试过程:import { reactive, readonly, isReactive, isReadonly } from "vue"; //vue 3.2.21

class Type {
constructor(code) {
this.code = code;
}

getCode() {
return this.code;
}
}

const simpler = readonly({ a: new Type(0), b: new Type(1) });
const moreComplex = reactive({ a: simpler.a, b: simpler.b });
console.log(Before: ${moreComplex.a === simpler.a}); // true
console.log(isReactive(moreComplex.a)); // false
console.log(isReadonly(moreComplex.a)); // true

moreComplex.a = simpler.b;

console.log(moreComplex.a); // { code: 1 }
console.log(simpler.b); // { code: 1 }

console.log(isReactive(moreComplex.a)); // true
console.log(isReadonly(moreComplex.a)); // false

moreComplex.a = simpler.a;

console.log(moreComplex.a); // { code: 0 }
console.log(simpler.a); // { code: 0 }

console.log(isReactive(moreComplex.a)); // true
console.log(isReadonly(moreComplex.a)); // false

console.log(After: ${moreComplex.a === simpler.a}); // false

从第一次赋值的时候就会触发set,set 里面会有一个调用 toRaw 函数的动作,所以这里就会把 .a 转换成普通的值对象,然后触发 get,从普通对象转成 reactive 对象了。最终判断的两者不相等只是类型不同,但是值仍然是相等的。

文章链接如下:https://www.jianshu.com/p/b9e659f00a45

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests