You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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响应式系统的“小冲突”:

  1. 你通过readonly(mainArray.value[0])返回的是一个Vue只读代理对象(Proxy)
  2. 当你把这个Proxy传给Immer的produce时,Immer会在内部处理这个代理对象,生成的新对象producePOJO会隐式携带一些来自原Vue代理的内部标记(比如响应式相关的__v_*属性,或者Immer自己的内部标记)
  3. Vue的响应式系统在检测对象是否需要转为响应式时,会识别到这些特殊标记,误以为这个对象已经是响应式的(或者是不需要被代理的对象),因此跳过了响应式转换的步骤
  4. 而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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 10:58:08