JSON.stringify 在 React Hooks 依赖项里的坑:一次复盘

JSON.stringify 在 React Hooks 依赖项里的坑:一次复盘

✨ 阅读时间约 4 分钟。

最近 review 代码,看到有人这样写依赖项:

javascript 复制代码
useEffect(() => {
  // do something with userProfile
}, [JSON.stringify(userProfile)]);

这种写法看起来能"深度比较"对象,实际上埋了个大坑。

为什么有人想用 JSON.stringify

项目中有个编辑表单,userProfile 是一个包含七八个字段的对象。每次用户输入,状态更新,组件重新渲染。开发者发现 useEffect 总是重复执行,想用 JSON.stringify 把对象转成字符串来做"深度比较",以为这样就能避免无谓的触发。

这个思路在直觉上说得通------字符串相同则对象相同嘛。但实际跑起来,问题比想象中更隐蔽。

现象:依赖项确实变了,但不是因为你想的原因

改完这行代码后,useEffect 确实不频繁触发了。但调试发现,某个下拉框的选项数据在某些情况下被清空了。进一步排查,useEffect 里的逻辑在错误的时间点执行,导致了数据覆盖。

更糟糕的是,如果 userProfile 里包含某些特殊值,整个应用直接崩溃。

根因:JSON.stringify 根本不是可靠的对象比较方式

MDN 上 JSON.stringify 的文档(MDN)明确说了几类值会被忽略或转换:

  • 函数(function)、undefined、Symbol 作为属性值会被忽略
  • Symbol 作为属性键会被完全忽略
  • NaN 和 Infinity 会变成 null
  • Date 对象会转成字符串("2024-01-01T00:00:00.000Z")
  • RegExp、Map、Set 会变成空对象 {} 或空结构
  • 如果对象里有循环引用,直接报错:"Converting circular structure to JSON"

这就意味着,两个完全不同的对象可能输出相同的字符串,而两个看起来相同的对象可能因为属性顺序不同而输出不同结果。

解决:要么拆分依赖,要么用专业的比较函数

正确做法是直接把用到的字段放进依赖数组:

javascript 复制代码
useEffect(() => {
  // 只用到了 name 和 age
}, [userProfile.name, userProfile.age]);

如果对象确实很大,可以用 lodash 的 isEqual 做深度比较,但要在 useEffect 里手动处理:

javascript 复制代码
const prevProfile = useRef(userProfile);

useEffect(() => {
  if (!isEqual(prevProfile.current, userProfile)) {
    // 业务逻辑
    prevProfile.current = userProfile;
  }
}, [userProfile]); // 依赖项还是得加,只是里面不做字符串转换

触类旁通:同类坑一起看清

除了依赖项,useMemo 和 useCallback 的依赖数组同样有这个问题。很多人以为用 JSON.stringify 包裹对象就能稳定缓存,实际上每次渲染生成的新对象转成字符串后内容相同,但引用变了,缓存依然失效。

另一个常见场景是表单状态。很多同学把所有表单项塞进一个对象,然后试图用 JSON.stringify 判断是否修改过。建议用专门的表单库(如 react-hook-form)或者维护一个 dirtyFields 标记对象,只追踪实际修改过的字段。

排查思路:为什么没按预期执行

如果有人反馈"看起来像被缓存了、没按预期执行(或执行时机不对)",可以按这个顺序排查:

  • 看依赖数组里是不是包了 JSON.stringify,以及依赖项里是否包含函数或 Symbol
  • 打印依赖项和字符串结果,确认"预期的变化是否被序列化忽略"
  • 用 Object.is 或简单比较确认引用是否变化,而不是内容变化
  • 临时改成显式依赖字段,验证行为是否回到预期

这套排查能快速判断问题是不是出在 JSON.stringify 的序列化边界上。

不同类型值如何影响比较

javascript 复制代码
const objA = {
  a: 1,
  u: undefined,
  fn: () => 1,
  s: Symbol('x'),
  d: new Date('2024-01-01T00:00:00Z'),
};

const objB = {
  a: 1,
  u: undefined,
  fn: () => 2,
  s: Symbol('y'),
  d: new Date('2024-01-01T00:00:00Z'),
};

JSON.stringify(objA) === JSON.stringify(objB); // true

这里 fn 和 s 的变化会被忽略,Date 会被序列化成相同的 ISO 字符串,所以两个对象会被误判为"相同"。如果你的依赖项靠它来判断变化,结果就是 "明明变了,但 effect 不触发"。

结论:依赖项表达的是"何时重新运行"

依赖项的本质是"引用比较"而非"内容比较"。React 不知道也不关心对象内部长什么样,它只关心这个引用是不是上一次渲染时的那个引用。

JSON.stringify 看似解决了比较问题,但它本身的行为规则太多太复杂,用来当依赖项无异于给自己埋炸弹。

相关推荐
远离UE42 小时前
UE5 Substrate Thin Film 源码拆析
开发语言·javascript·ue5
keyipatience2 小时前
JavaScript学习(1)书写位置,变量、数据类型、运算符
开发语言·前端·javascript·css·学习
SensorFlow6 小时前
曝光埋点别放在组件渲染里:连续可见与去重范围怎么验收
javascript
harris14815435297586 小时前
能解码不等于能信任:JWT 的签名到底验的是什么
javascript
liqingdi4376 小时前
[SWPUCTF 2023 秋季新生赛]Easy SMC
服务器·前端·javascript
日耗10亿Token被老板赶出门6 小时前
REA 火了:一个 Electron 按钮的调用链,它能追到哪一步?
javascript·electron
青柠之夏cc7 小时前
Laravel服务容器底层原理揭秘
javascript·rpc·laravel
变与不变8068 小时前
防抖节流详解
开发语言·前端·javascript
用户6602676118798 小时前
HTTP 200,页面仍是旧状态:定位一次前后端字段错配
javascript
用户72526496629698 小时前
浏览器为什么加载不了超大图片
前端·javascript