为何Immer produce生成的对象与JSON.parse生成的对象在Vue中表现不同?
为何Immer produce生成的对象与JSON.parse生成的对象在Vue中表现不同?
我来帮你拆解这个遇到的奇怪问题,先从你复现的代码说起,再分析原因和解决方案。
首先是可复现的完整Vue SFC代码:
<script setup> import { ref, readonly, computed, toRaw } from 'vue' import { produce } from 'immer'; const mainArray = ref([ { nested: { user: { id: 1 } } } ]) // 模拟组件中对传入props做readonly包装的场景 const model = computed({ get: () => readonly(mainArray.value[0]), set: (value) => { mainArray.value[0] = value } }) let nextValue = 2; function updateViaJSON() { const jsonPOJO = JSON.parse(JSON.stringify(model.value)); jsonPOJO.nested.user.id = nextValue++ console.log('before model: ', jsonPOJO) model.value = jsonPOJO console.log('model value: ', model.value) } function updateViaProduce() { const producePOJO = produce(model.value, (draft) => { draft.nested.user.id = nextValue++ }); console.log('before model: ', producePOJO) model.value = producePOJO console.log('model value: ', model.value) } </script> <template> <div> {{ model }} </div> <button type="button" @click="updateViaJSON"> Update via JSON </button> <button type="button" @click="updateViaProduce"> Update via produce </button> </template>
现象回顾
你会发现:
- 点击「Update via JSON」后,
model.value会变成Vue的响应式Proxy对象,视图也会正常更新 - 点击「Update via Produce」后,
producePOJO在赋值前明明是普通POJO,但赋值给model.value后,它依然是普通对象,不会被Vue转为响应式Proxy,视图也不会正确追踪状态变化
问题根源
这其实是Immer和Vue响应式系统的“小冲突”:
- 你通过
readonly(mainArray.value[0])返回的是一个Vue只读代理对象(Proxy) - 当你把这个Proxy传给Immer的
produce时,Immer会在内部处理这个代理对象,生成的新对象producePOJO会隐式携带一些来自原Vue代理的内部标记(比如响应式相关的__v_*属性,或者Immer自己的内部标记) - Vue的响应式系统在检测对象是否需要转为响应式时,会识别到这些特殊标记,误以为这个对象已经是响应式的(或者是不需要被代理的对象),因此跳过了响应式转换的步骤
- 而
JSON.parse(JSON.stringify(model.value))生成的是完全纯净的普通JS对象,没有任何额外标记,所以Vue会正常把它转为响应式Proxy
解决方案
这里有几个可行的解决办法,按推荐程度排序:
1. 先取出只读代理的原始对象再传给Immer
用Vue的toRaw方法,直接获取model.value(只读代理)背后的原始普通对象,再交给Immer处理:
import { toRaw } from 'vue' function updateViaProduce() { // 先取出原始对象,再传给produce const rawModel = toRaw(model.value) const producePOJO = produce(rawModel, (draft) => { draft.nested.user.id = nextValue++ }); console.log('before model: ', producePOJO) model.value = producePOJO console.log('model value: ', model.value) }
这个方法最高效,因为toRaw不会做深拷贝,只是直接返回原始对象。
2. 把Immer生成的对象转为纯净POJO
如果不方便用toRaw,可以把produce生成的对象转成纯净的普通对象,比如用Object.assign(浅拷贝,适合单层对象)或者JSON序列化(深拷贝,适合多层嵌套对象):
// 浅拷贝适合单层对象 const purePOJO = Object.assign({}, producePOJO) // 深拷贝适合多层嵌套对象 const purePOJO = JSON.parse(JSON.stringify(producePOJO)) model.value = purePOJO
3. 直接操作原始数组元素(绕过只读代理)
其实你可以绕过model的只读代理,直接操作mainArray.value[0],这样Immer处理的就是普通对象了:
function updateViaProduce() { mainArray.value[0] = produce(mainArray.value[0], (draft) => { draft.nested.user.id = nextValue++ }); }
这个方法更直接,生成的对象是普通POJO,Vue会正常转为响应式。
验证效果
修改后再点击「Update via Produce」,你会发现model.value会变成Vue的响应式Proxy,视图也会正常更新,和「Update via JSON」的表现一致了。
备注:内容来源于stack exchange,提问作者captainskippah
相关产品推荐
相关产品推荐

