ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript Object 对象比较全解:引用对比、浅对比、深对比与 React 渲染优化实战

JavaScript Object 对象比较全解:引用对比、浅对比、深对比与 React 渲染优化实战 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本篇精读基于前端精读周刊第 157 期《如何比较 Object 对象》展开。对象比较是前端开发中绕不开的基础能力本篇系统梳理引用对比、手动对比、浅对比shallow equal与深对比deep equal四种方案的实现原理、适用场景与性能代价并结合仓库内 React 相关篇章说明它们在 props 比较、shouldComponentUpdate、useMemo与 Redux 数据流中的真实应用与优化建议。读完你将掌握如何选择正确的对象比较方式、如何手写浅对比与深对比工具函数以及如何用引用对比思维根治组件重复渲染问题。引言为什么 Object 比较是前端基础知识JavaScript 中、对 Object 类型比较的是引用内存地址而非内容。这意味着两个结构完全相同的对象用比较结果也是false。这一语言特性直接决定了 React 渲染优化的底层逻辑React 判断 props 是否变化靠的是引用对比Redux 判断 state 是否更新靠的也是引用对比。理解对象比较的四种方式是写好高性能 React 组件的前提。本文基于 精读《如何比较 Object 对象》 展开原文系统介绍了四种对比方法引用对比、手动对比、浅对比、深对比。一、引用对比语言内置的三种方式下面三种对比方式用于 Object 时仅当引用相同才返回true严格相等宽松相等对 Object 而言与行为一致Object.is()const hero1 { name: Batman, }; const hero2 { name: Batman, }; hero1 hero1; // true hero1 hero2; // false hero1 hero1; // true hero1 hero2; // false Object.is(hero1, hero1); // true Object.is(hero1, hero2); // false从源码结构看三个对象字面量hero1与hero2内容完全相同但因为它们是两个独立的内存对象所以所有引用对比的结果都是false。需要说明的是Object.is()与在绝大多数情况下行为一致仅有两处细微差异Object.is(NaN, NaN)为true而NaN NaN为falseObject.is(-0, 0)为false而-0 0为true。在日常 Object 引用对比场景中三者可以等价使用。二、手动对比面向业务的定制比较写一个自定义函数按照对象内容做定制对比是最直接、最贴合业务的方案function isHeroEqual(object1, object2) { return object1.name object2.name; } const hero1 { name: Batman, }; const hero2 { name: Batman, }; const hero3 { name: Joker, }; isHeroEqual(hero1, hero2); // true isHeroEqual(hero1, hero3); // false手动对比的关键价值在于只比较业务关心的字段。如果对象 key 不多或者在特殊业务场景需要忽略某些字段例如比较时跳过updatedAt这类易变字段时这种方案其实非常实用。但这种方案不够自动化每新增一个字段、每换一个业务实体都要重写一个比较函数。也正是这种定制的繁琐催生了通用的浅对比。三、浅对比逐属性引用对比的自动化平衡浅对比函数写法很多但其效果标准是统一的先比较属性数量再对每个属性做引用对比!。下面给出一种经典实现function shallowEqual(object1, object2) { const keys1 Object.keys(object1); const keys2 Object.keys(object2); // 属性数量不同必然不等 if (keys1.length ! keys2.length) { return false; } // 逐个属性做引用对比 for (let key of keys1) { if (object1[key] ! object2[key]) { return false; } } return true; }可以看到浅对比就是将对象的每个属性进行引用对比属性值为基本类型时比较值属性值为对象时比较引用。这算是一种性能上的平衡——既避免了JSON.stringify之类的全量序列化开销又能自动覆盖全部字段。使用示例const hero1 { name: Batman, realName: Bruce Wayne, }; const hero2 { name: Batman, realName: Bruce Wayne, }; const hero3 { name: Joker, }; shallowEqual(hero1, hero2); // true shallowEqual(hero1, hero3); // false如果对象层级再多一层比如hero.address.city这种嵌套结构浅对比就失效了——因为浅对比只比较第一层属性的引用而两个嵌套对象哪怕内容一致其引用也必然不同。此时需要深对比。四、深对比递归对比全部层级深对比就是递归对比对象的所有简单对象值遇到复杂对象就逐个 key 继续对比以此类推直到叶子节点的基本类型再用!判定。下面是一种实现方式function deepEqual(object1, object2) { const keys1 Object.keys(object1); const keys2 Object.keys(object2); if (keys1.length ! keys2.length) { return false; } for (const key of keys1) { const val1 object1[key]; const val2 object2[key]; const areObjects isObject(val1) isObject(val2); if ( (areObjects !deepEqual(val1, val2)) || (!areObjects val1 ! val2) ) { return false; } } return true; } function isObject(object) { return object ! null typeof object object; }可以看到只要遇到 Object 类型的 key就会递归调用一次deepEqual进行比较对于简单类型字符串、数字、布尔等直接用!引用对比。值得注意的是数组类型也满足typeof object object的条件且Object.keys可以作用于数组object[key]也可作用于数组因此数组和对象都可以采用相同方式处理无需额外分支。有了深对比再也不用担心复杂对象的比较了const hero1 { name: Batman, address: { city: Gotham, }, }; const hero2 { name: Batman, address: { city: Gotham, }, }; deepEqual(hero1, hero2); // true但深对比会造成性能损耗不要小看递归的作用在对象树复杂时深对比会递归遍历整棵树的所有节点对象越深、属性越多损耗越大甚至会导致严重的性能问题。深对比的边界与陷阱从上面的实现可以推断出几个需要警惕的边界情况循环引用当对象存在自引用如a.self a时上述朴素递归实现会无限递归导致栈溢出。工业级实现如 lodash 的isEqual会用栈记录已访问对象来解决。null与undefinedisObject中的object ! null判断排除了nulltypeof null object是历史遗留陷阱但两个值为null的属性会被当作基本类型走!对比这是正确的。函数、Symbol、Date、RegExp 等特殊对象朴素实现下函数、Symbol 走!引用对比Date、RegExp 会被当作对象递归比较其内部属性很可能得出不相等的错误结论。这些边界说明深对比的深是有尽头的实际工程中应优先用成熟的库实现而非手写递归。五、精读四种对比在 React 渲染优化中的真实取舍原文档的精读部分将四种对比方法放进了 React 渲染优化的真实场景中给出了非常实用的取舍原则。5.1 引用对比props 比较的唯一正规军引用对比是最常用的一般在做 props 比较时只允许使用引用对比this.props.style ! nextProps.style;如果看到有深对比的地方一般就要有所警觉这里是真的需要深对比吗是不是其他地方写法有问题导致的。比如在某处看到这样的代码deepEqual(this.props.style, nextProps.style);这很可能是父组件一处随意拼写导致的——每次渲染都内联创建新对象const Parent () { return Child style{{ color: red }} /; };一个只解决局部问题的同学可能会采用deepEqual兜底OK 这样也能解决问题但一个有全局感的同学会这样解决问题——把问题消灭在源头让引用对比重新成立this.props.style nextProps.style;const Parent () { const style useMemo(() ({ color: red }), []); return Child style{style} /; };从性能上来看Parent定义的style只会执行一次且下次渲染几乎没有对比损耗依赖为空数组子组件引用对比性能最佳这样的组合一定优于deepEqual的例子。5.2 浅对比判断组件是否重渲染的常用手段浅对比在判断组件是否重渲染时很常用shouldComponentUpdate(nextProps) { return !shallowEqual(this.props, nextProps) }为什么浅对比在这里是安全且合理的原因是this.props这个对象引用的变化在逻辑上是无需关心的——应用只会使用到this.props[key]这一层级props 的消费点永远是某个属性而非props 整体。再考虑到 React 组件生态下Immutable 的上下文保证了任何对象子属性变化一定导致对象整体引用变化可以放心地进行浅对比。这一思路在仓库内其他篇章中得到了印证在 精读《Function VS Class 组件》 中可以看到class Button extends React.PureComponent {}就自带shallowEqual的shouldComponentUpdate而 Function Component 则用React.memo达到同等效果。在 精读《Function Component 入门》 中进一步说明使用memo包裹的组件会在自身重渲染时对每一个props项进行浅对比如果引用没有变化就不会触发重渲染useMemo则能做更细粒度的渲染优化——当函数组件整体用到了A、B两个 props 而渲染仅用到B时useMemo方案下A的变化不会导致重渲染这是memo做不到的。5.3 Immutable 结构共享浅对比的基石浅对比之所以能在 React / Redux 生态中放心使用背后是 Immutable 数据流的保证。精读《Immutable 结构共享》 指出redux 判断数据更新的条件是对象引用是否变化且要求当修改对象子属性时父级对象的引用也要一并修改。用Object.assign即可实现这一效果objA objB // false父对象引用已变 objA.a objB.a // true未修改的属性引用共享 objA.b objB.b // true这种根节点引用改变、未修改节点引用不变的结构共享恰好让浅对比在判断内容是否实质变化时既快速又准确——只要顶层引用变了就说明数据确实更新了。这也是为什么原文断言最少见的就是手动对比和深对比如果你看到一段代码中使用了深对比大概率这段代码可以被优化为浅对比。5.4 深对比在 Hooks 数据流中的陷阱深对比并非一无是处但必须谨慎使用。精读《React Hooks 数据流》 记录了一个经典的深对比陷阱useSelector中假设user对象在每次数据流更新时引用都会变化此时shallowEqual不起作用换成deepEqual深对比后重渲染不那么频繁了但user的引用依然会变——因为useSelector选择函数在全局 reducer 更新时仍会重新执行拿到的是新引用。因此深对比必须与useDeepMemo结合使用才能保证返回结果的引用稳定否则会引入隐藏的引用漂移 bug。这个案例再次印证了原文档的结论看到deepEqual与useSelector同时出现就要反问自己其返回值的引用会不会发生意外变化六、总结四种对比的选用优先级虽然本文总结了 4 种比较 Object 对象的方式但在实际项目中应该遵循以下优先级方案对比粒度性能自动化程度推荐场景引用对比/Object.is整体引用最优O(1)语言内置props 比较、Immutable 数据流下的状态判断浅对比shallowEqual第一层属性优O(n)通用函数React.memo、PureComponent、shouldComponentUpdate、redux 判断手动对比定制函数业务字段优O(k)定制key 少或需忽略部分字段的特殊业务深对比deepEqual全部层级差O(树节点数)递归通用函数嵌套数据校验等低频场景慎用于渲染路径实践原则尽可能使用引用对比其次是浅对比和手动对比最坏的情况是使用深对比。深对比不仅是性能问题更是设计问题的信号——它往往意味着数据流或组件写法存在隐患值得回到源头修复而不是在比较环节打补丁。延伸阅读本文涉及的对象比较原理与仓库内以下篇章互为补充精读《Function VS Class 组件》PureComponent自带 shallowEqual 的shouldComponentUpdate以及React.memo/useMemo的替代写法精读《Function Component 入门》memo的 props 浅对比与useMemo的细粒度渲染优化精读《Immutable 结构共享》对象引用变化与结构共享原理浅对比的生态基石精读《React Hooks 数据流》useSelector与 shallowEqual/deepEqual 组合时的引用稳定性陷阱精读《setState 做了什么》React 状态更新的触发与合并机制理解引用对比如何影响重渲染赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Java对象差异对比让对象比较变得简单高效Java对象差异对比让对象比较变得简单高效 想象一下你正在开发一个版本控制系统需要精确追踪用户配置的每一次变更。传统方法需要编写大量重复的反射代码lodash对象深比较isEqualWith自定义比较规则lodash对象深比较isEqualWith自定义比较规则 你是否曾遇到JavaScript对象比较的困扰明明内容相同的对象用 却返回 false前端后端jsdiff JSON比对高级应用canonicalize实现复杂对象的深度比较jsdiff JSON比对高级应用canonicalize实现复杂对象的深度比较 jsdiff是一个强大的JavaScript文本差异比较库其中 diffJ开发者工具版本控制创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表