为什么在Vue3响应式源码中,使用Reflect.set时先赋值再返回能够解决更新问题?

新手上路,请多包涵

关于自学vue3响应式源码-proxy中的一个问题(Reflect.set)

最近在看vue3响应式源码,自己手写时发现了一个情况是

export const reactive = (target) => {
    return new Proxy(target, {
        get(target, prop, recevier) {
            //依赖收集
            track(target, prop);
            return Reflect.get(...arguments);
        },
        set(target, prop, value, recevier) {
            let res = Reflect.set(...arguments);
            //触发依赖更新
            trigger(target, prop);
            //这里如果直接return Reflect.set(...arguments),则会有更新问题
            //比如在setTimeout中同时更新一个对象的两个属性,则计时器结束之后只会更新第一个属性,第二个不触发更新
            //先赋值res,再返回,则正常
            return res; 
        },

    })
}
   let animal = reactive(
        {
            name: 'dog',
            age: 2
        }
    )
    effect(() => {
        document.querySelector('#app').innerHTML = `
        <div>
            <h2>name: ${animal.name}</h2>
            <h2>age: ${animal.age}</h2>
        </div>
        `
    })

    setTimeout(()=>{
        animal.name = 'cat'//更新
        animal.age = 3//直接return Reflect.set不会更新
    },2000)

改为let res = Reflect.set(...arguments);再返回则正常

阅读 895
3 个回答

animal.name = 'cat' 首次 trigger 时,animal 对象还未更新,拿到的是 {name: 'dog', age: 2}, 执行完 effect 后,使用 Reflect.set(...arguments) 进行设置,变为了 {name: 'cat', age: 2}

animal.age = 3 再次 trigger 时,拿到的 animal 值为 {name: 'cat', age: 2},表现为“第二个不触发更新”,最后使用不触发更新的 Reflect.set(...arguments) 进行设置,变为了 {name: 'cat', age: 3},页面无变化

如楼上所说,其实在第一次 trigger 时,并没有更新,数据还是旧值,当你第二次 trigger 时,第一次此时只有 name 的值是最新的,也就是 cat, 而 age 此时并没有被赋值到最新值3!

因为你是先 trigger 后才通过 Reflect.set 更新值的,而你把 Reflect.set 提到 trigger 前执行,则是先赋值新值后再去渲染,所以是能够得到最新的值!

在 Vue 3 响应式系统中,使用 Proxy 和 Reflect 来实现对象的响应式是一个关键技术点。关于在 Reflect.set 中先赋值再返回能够解决更新问题,主要原因涉及到 JavaScript 的执行顺序和响应式依赖追踪与触发机制。

问题分析
以下是问题的简化示例:

export const reactive = (target) => {
    return new Proxy(target, {
        get(target, prop, receiver) {
            //依赖收集
            track(target, prop);
            return Reflect.get(...arguments);
        },
        set(target, prop, value, receiver) {
            let res = Reflect.set(...arguments);
            //触发依赖更新
            trigger(target, prop);
            return res;
        },
    });
};

let animal = reactive({
    name: 'dog',
    age: 2
});

effect(() => {
    document.querySelector('#app').innerHTML = `
        <div>
            <h2>name: ${animal.name}</h2>
            <h2>age: ${animal.age}</h2>
        </div>
    `;
});

setTimeout(() => {
    animal.name = 'cat'; // 更新
    animal.age = 3; // 直接 return Reflect.set 不会更新
}, 2000);

问题原因:
如果直接 return Reflect.set(...arguments);,当多个属性在同一个事件循环中更新时,Vue 可能无法正确追踪到所有的变化。这是因为 Reflect.set 返回一个布尔值,表示设置操作是否成功。如果在 set 捕获器中直接返回这个布尔值而没有提前赋值给变量,依赖更新机制(如 trigger)可能不会在所有情况下都能正确触发更新。

解决方法:
通过将 Reflect.set 的结果先赋值给一个变量,然后再返回这个变量,可以确保在触发依赖更新之前,设置操作已经完成。这样可以确保所有的依赖更新都能被正确触发。

详细解释:

1.Reflect.set 的返回值:
  a.Reflect.set 的返回值是一个布尔值,表示设置操作是否成功。
  b.如果直接返回这个布尔值,依赖更新机制可能会错过一些变化,特别是在多个属性同时更新的情况下。
2.提前赋值的好处:
  a. 提前赋值确保了设置操作的完成,然后再触发依赖更新。
  b.这使得依赖更新机制能够正确捕捉到所有的变化,并在所有依赖的地方正确更新。    

直接返回 Reflect.set:

set(target, prop, value, receiver) {
    return Reflect.set(...arguments); // 可能导致更新问题
}

先赋值再返回:

set(target, prop, value, receiver) {
    let res = Reflect.set(...arguments); // 确保设置操作完成
    trigger(target, prop); // 触发依赖更新
    return res; // 返回设置结果
}

通过上述方法,确保在触发依赖更新之前,设置操作已经完成,从而避免了在多个属性同时更新时可能遇到的问题。这样可以确保 Vue 的响应式系统能够正确工作,捕捉到所有的变化并及时更新。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏