资讯动态

Zustand shallow 全面解析:用浅比较优化 React 状态选择器与订阅逻辑

发布时间:2026/9/18 6:18:48 来源:尧图企业网站定制
Zustand shallow 全面解析用浅比较优化 React 状态选择器与订阅逻辑【免费下载链接】zustand Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustandshallow是 Zustand 提供的轻量级比较工具它基于Object.is对两个值做顶层属性级top-level的浅比较用于快速判断简单数据结构是否发生变化。在 Zustand 的 React 绑定、useShallow选择器缓存、subscribeWithSelector订阅去重以及useStoreWithEqualityFn自定义相等性判断中它都是最核心的默认/推荐比较函数。读完本文你将掌握shallow的类型签名、四种数据类型的比较规则、底层实现原理含源码逐段解析以及它在 Zustand 各模块中的真实应用方式。什么是shallowshallow的功能非常聚焦对不包含嵌套对象或数组的简单数据结构做快速比较。它只比较两个值的顶层属性一旦发现顶层键或值不一致立即返回false如果两个值在顶层完全一致则返回true。const equal shallow(a, b)[!NOTE]shallow追求的是“快速比较”使用时必须牢记它的局限性嵌套结构的对象/数组不会被递归比较因此嵌套内容相同但引用不同的对象会被判定为不相等详见故障排查一节。类型签名shallowT(a: T, b: T): booleana参与比较的第一个值。b参与比较的第二个值。返回值当a与b基于顶层属性的浅比较结果相等时返回true否则返回false。shallow与Object.is的区别shallow的第一个快速路径就是Object.is见下文源码分析但二者定位完全不同比较方式原始值相同引用的对象内容相同但引用不同的对象Object.is按值比较truefalseshallow按值比较true顶层属性相同时为true也就是说Object.is是“引用/值”的严格同一性判断而shallow在Object.is判等失败后还会进一步对顶层属性逐项比较这正是它作为状态变化检测工具的价值所在。使用方法比较原始类型Primitives比较string、number、boolean、BigInt等原始值时Object.is与shallow的结果完全一致相同值返回true。这是因为原始值按实际值而非引用进行比较。const stringLeft John Doe const stringRight John Doe Object.is(stringLeft, stringRight) // - true shallow(stringLeft, stringRight) // - true const numberLeft 10 const numberRight 10 Object.is(numberLeft, numberRight) // - true shallow(numberLeft, numberRight) // - true const booleanLeft true const booleanRight true Object.is(booleanLeft, booleanRight) // - true shallow(booleanLeft, booleanRight) // - true const bigIntLeft 1n const bigIntRight 1n Object.is(bigIntLeft, bigIntRight) // - true shallow(bigIntLeft, bigIntRight) // - true这一点在 shallow 的单元测试中也有直接覆盖shallow(1, 1)、shallow(zustand, zustand)、shallow(true, true)均为true而shallow(1, 2)、shallow(zustand, redux)、shallow(true, false)均为false。比较对象Objects两个内容相同但引用不同的对象Object.is返回false而shallow会逐项检查顶层属性firstName、lastName、age及其值全部相同则返回trueconst objectLeft { firstName: John, lastName: Doe, age: 30, } const objectRight { firstName: John, lastName: Doe, age: 30, } Object.is(objectLeft, objectRight) // - false shallow(objectLeft, objectRight) // - true需要特别强调的是只要顶层键集合不同哪怕值有重叠也会返回false。测试 tests/vanilla/shallow.test.tsx 验证了三种场景shallow({ foo: bar, asd: 123 }, { foo: bar, asd: 123 }) // - true键值完全一致 shallow({ foo: bar, asd: 123 }, { foo: bar, foobar: true }) // - false键不一致 shallow({ foo: bar, asd: 123 }, { foo: bar, asd: 123, foobar: true }) // - false多了一个键比较 Setshallow比较两个Set时检查它们是否都是Set实例且包含相同元素不要求插入顺序一致内容一致则返回trueconst setLeft new Set([1, 2, 3]) const setRight new Set([1, 2, 3]) Object.is(setLeft, setRight) // - false shallow(setLeft, setRight) // - true测试还覆盖了更多细节tests/vanilla/shallow.test.tsxshallow(new Set([bar, 123]), new Set([123, bar])) // - true顺序无关 shallow(new Set([bar, 123]), new Set([bar, 2])) // - false元素不同 shallow(new Set([bar, 123]), new Set([bar, 123, true])) // - false元素个数不同 // 元素按引用比较同一对象引用才相等 const obj {} const obj2 {} shallow(new Set([obj]), new Set([obj])) // - true shallow(new Set([obj]), new Set([obj2])) // - false比较 Mapshallow比较两个Map时检查键值对是否完全一致同样不要求插入顺序一致一致则返回trueconst mapLeft new Map([ [1, one], [2, two], [3, three], ]) const mapRight new Map([ [1, one], [2, two], [3, three], ]) Object.is(mapLeft, mapRight) // - false shallow(mapLeft, mapRight) // - true测试 tests/vanilla/shallow.test.tsx 验证了键值完全一致为true、插入顺序不同仍为true、键或值不同为false、大小不同为false并且键按引用比较——两个不同的空对象作为键时结果为falseconst obj {} const obj2 {} shallow( new Mapobject, unknown([[obj, foo]]), new Mapobject, unknown([[obj2, foo]]), ) // - false键的引用不同源码级原理shallow的完整实现shallow的核心实现位于 src/vanilla/shallow.ts整个判断流程可分为五个阶段export function shallowT(valueA: T, valueB: T): boolean { // 阶段一Object.is 快速路径 if (Object.is(valueA, valueB)) { return true } // 阶段二非对象短路 if ( typeof valueA ! object || valueA null || typeof valueB ! object || valueB null ) { return false } // 阶段三原型一致性检查 if (Object.getPrototypeOf(valueA) ! Object.getPrototypeOf(valueB)) { return false } // 阶段四可迭代对象分派 if (isIterable(valueA) isIterable(valueB)) { if (hasIterableEntries(valueA) hasIterableEntries(valueB)) { return compareEntries(valueA, valueB) } return compareIterables(valueA, valueB) } // 阶段五按纯对象处理 return compareEntries( { entries: () Object.entries(valueA) }, { entries: () Object.entries(valueB) }, ) }阶段一Object.is快速路径两个值引用相同或原始值相等时直接返回true这是最昂贵的检查深比较之前的最快退出。阶段二非对象短路只要有一方不是object类型或是null在已经通过Object.is的前提下必然不相等直接返回false。函数不满足typeof object因此两个内容相同但声明不同的函数也会在这里返回false测试 tests/vanilla/shallow.test.tsx 验证了同一函数引用比较为true、不同函数声明为false。阶段三原型一致性检查Object.getPrototypeOf(valueA) ! Object.getPrototypeOf(valueB)两个对象原型不同直接返回false。这是文档中“不同原型对象比较”故障排查场景的实现来源详见下文。阶段四可迭代对象分派实现首先用辅助函数判断类型src/vanilla/shallow.tsconst isIterable (obj: object): obj is Iterableunknown Symbol.iterator in obj const hasIterableEntries ( value: Iterableunknown, ): value is Iterableunknown { entries(): Iterable[unknown, unknown] } // HACK: avoid checking entries type entries in value若双方都具备entries()方法如Map、Set、URLSearchParams、纯对象包装走compareEntries否则走compareIterables如生成器、数组。compareEntries逐项比较键值对src/vanilla/shallow.ts先把非Map的值转成Map若size不同直接返回false再遍历第一个Map的每一项要求第二个Map中存在相同键且值通过Object.is相等const compareEntries (valueA, valueB) { const mapA valueA instanceof Map ? valueA : new Map(valueA.entries()) const mapB valueB instanceof Map ? valueB : new Map(valueB.entries()) if (mapA.size ! mapB.size) { return false } for (const [key, value] of mapA) { if (!mapB.has(key) || !Object.is(value, mapB.get(key))) { return false } } return true }这就是为什么文档示例中两个“内容相同”的Set/Map会返回trueSet通过entries()拿到[value, value]键值对后统一按条目比较因此无序性天然成立。compareIterables按顺序逐项比较src/vanilla/shallow.ts适用于数组、生成器这类“有序可迭代对象”。它同时推进两个迭代器逐项做Object.is任何一项不同立即返回false最后要求双方同时迭代完毕const compareIterables (valueA, valueB) { const iteratorA valueA[Symbol.iterator]() const iteratorB valueB[Symbol.iterator]() let nextA iteratorA.next() let nextB iteratorB.next() while (!nextA.done !nextB.done) { if (!Object.is(nextA.value, nextB.value)) { return false } nextA iteratorA.next() nextB iteratorB.next() } return !!nextA.done !!nextB.done }阶段五纯对象按Object.entries比较普通对象没有Symbol.iterator进入最后的兜底分支把Object.entries(value)包装成伪entries()迭代器再交给compareEntries逐键比较顶层属性。注意这里每个属性值仍然只做Object.is引用比较——这正是“嵌套对象内容相同但引用不同时返回false”的根本原因详见故障排查。额外覆盖的数据类型由于实现基于“迭代器 entries”抽象shallow还天然支持了文档示例之外的多种类型均由 tests/vanilla/shallow.test.tsx 佐证数组走compareIterables有序且敏感于顺序shallow([1, 2, 3], [1, 2, 3])为trueshallow([1, 2, 3], [2, 3, 1])为false数组内嵌对象同样只做引用比较两个内容相同的新对象数组结果为falseURLSearchParams走compareEntries键值对比较且顺序无关shallow(new URLSearchParams({ a: a }), new URLSearchParams({ a: a }))为true生成器/纯可迭代对象走compareIterablestests/vanilla/shallow.test.tsx两个产出相同序列的生成器比较为true跨类型比较一律为falsetests/vanilla/shallow.test.tsx对象 vs 数组、对象 vs Set、数组 vs Map 等混合结构均返回false嵌套数组引用优化shallow([arr, 1], [arr, 1])同一个arr引用为true回归用例 #2794。在 Zustand 中的应用场景shallow本身是一个通用比较函数但它之所以在 Zustand 中高频出现是因为它是以下三个优化场景的“粘合剂”。场景一useShallow—— 记忆化选择器src/react/shallow.ts 基于shallow实现了useShallowHook它把用户传入的selector包装成“带缓存的选择器”缓存上一次的选择结果用shallow判断新结果与缓存是否相等相等则继续返回旧引用避免每次渲染都产生新引用导致组件无限重渲染export function useShallowS, U(selector: (state: S) U): (state: S) U { const prev React.useRefU(undefined) return (state) { const next selector(state) return shallow(prev.current, next) ? (prev.current as U) : (prev.current next) } }典型用法是在选择器中构造新对象/数组时包一层useShallow完整示例见 useShallow 文档const names useBearFamilyMealsStore( useShallow((state) Object.keys(state)), )自 v5 起选择器若在每次渲染返回新引用默认的Object.is相等性检查会触发 “Maximum update depth exceeded” 无限循环用useShallow包裹后比较的是包装对象的顶层属性按引用从而打破循环详见 v5 迁移指南中的稳定选择器输出要求。场景二subscribeWithSelector的equalityFnsrc/middleware/subscribeWithSelector.ts 允许对 store 的subscribe传入选择器与自定义equalityFn默认值为Object.isconst equalityFn options?.equalityFn || Object.is当你订阅的切片是每次重新构建的对象时把equalityFn设为shallow即可让订阅只在顶层属性真正变化时触发避免频繁回调import { subscribeWithSelector } from zustand/middleware import { shallow } from zustand/shallow const unsubscribe useStore.subscribe( (state) ({ a: state.a, b: state.b }), (slice, prevSlice) console.log(changed, slice, prevSlice), { equalityFn: shallow }, )场景三useStoreWithEqualityFn的第三个参数zustand/traditional中的useStoreWithEqualityFn(store, selectorFn, equalityFn)同样接收自定义相等性函数见 useStoreWithEqualityFn 文档传入shallow即可实现“选择器返回新对象但顶层相等时不重渲染”的效果与useShallow的目标一致。导出路径与包结构shallow及useShallow通过 src/shallow.ts 统一导出export { shallow } from ./vanilla/shallow.ts export { useShallow } from ./react/shallow.ts在包中对应的导入路径为import { shallow } from zustand/shallow // 仅比较函数 import { useShallow } from zustand/react/shallow // React Hook 版本测试文件中的import { shallow } from zustand/shallowtests/vanilla/shallow.test.tsx印证了第一条导出路径的有效性。故障排查嵌套对象比较返回false即使内容完全一致shallow只比较顶层属性不检查嵌套对象或深层属性本质上是对每个顶层属性做引用比较。在下面的例子中两个对象的address都是嵌套对象虽然内部内容逐字相同但各自拥有不同的引用因此shallow判定它们不同返回falseconst objectLeft { firstName: John, lastName: Doe, age: 30, address: { street: Kulas Light, suite: Apt. 556, city: Gwenborough, zipcode: 92998-3874, geo: { lat: -37.3159, lng: 81.1496, }, }, } const objectRight { firstName: John, lastName: Doe, age: 30, address: { street: Kulas Light, suite: Apt. 556, city: Gwenborough, zipcode: 92998-3874, geo: { lat: -37.3159, lng: 81.1496, }, }, } Object.is(objectLeft, objectRight) // - false shallow(objectLeft, objectRight) // - false移除address属性后顶层属性全部是原始值浅比较即可正常工作const objectLeft { firstName: John, lastName: Doe, age: 30, } const objectRight { firstName: John, lastName: Doe, age: 30, } Object.is(objectLeft, objectRight) // - false shallow(objectLeft, objectRight) // - true结论与应对如果状态结构存在嵌套对象请对嵌套数据使用JSON.stringify之外的专用深比较库或在放入 store 前保证嵌套对象引用稳定复用同一引用或拆分顶层状态让每个选择器只取原始值字段。比较不同原型的对象shallow会先检查两个对象是否具有相同原型Object.getPrototypeOf(a) Object.getPrototypeOf(b)见 src/vanilla/shallow.ts原型引用不同则直接返回false。[!IMPORTANT] 用对象字面量{}或new Object()创建的对象默认继承自Object.prototype而用Object.create(proto)创建的对象继承自传入的proto——它可能不是Object.prototype。const a Object.create({}) // - prototype 是 {} const b {} // - prototype 是 Object.prototype shallow(a, b) // - false这也解释了为什么Date等内置类型不在支持范围内两个不同时刻的Date原型虽相同但其没有entries()也非纯对象可迭代测试 tests/vanilla/shallow.test.tsx 明确标注Date为unsupported cases比较结果不可预期不应依赖。小结shallow是 Zustand 性能优化体系中最基础也最常用的一环它以Object.is为快速路径配合原型检查、可迭代对象分派compareEntries/compareIterables和Object.entries兜底实现了对原始类型、普通对象、Set、Map、数组、URLSearchParams、生成器等多种数据结构的顶层浅比较。牢记它的边界——嵌套结构按引用比较、不同原型直接不等——就能在useShallow、subscribeWithSelector与useStoreWithEqualityFn中安全地用它消除无谓的重渲染与订阅回调。想深入验证其行为可直接阅读 src/vanilla/shallow.ts、src/react/shallow.ts 及配套测试 tests/vanilla/shallow.test.tsx。【免费下载链接】zustand Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustand创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价