Zustand shallow 全面解析:用浅比较优化 React 状态选择器与订阅逻辑
2026/9/18 6:18:42 网站建设 项目流程

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

【免费下载链接】zustand🐻 Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustand

shallow是 Zustand 提供的轻量级比较工具,它基于Object.is对两个值做顶层属性级(top-level)的浅比较,用于快速判断简单数据结构是否发生变化。在 Zustand 的 React 绑定、useShallow选择器缓存、subscribeWithSelector订阅去重以及useStoreWithEqualityFn自定义相等性判断中,它都是最核心的默认/推荐比较函数。读完本文,你将掌握shallow的类型签名、四种数据类型的比较规则、底层实现原理(含源码逐段解析),以及它在 Zustand 各模块中的真实应用方式。

什么是shallow

shallow的功能非常聚焦:对不包含嵌套对象或数组的简单数据结构做快速比较。它只比较两个值的顶层属性,一旦发现顶层键或值不一致,立即返回false;如果两个值在顶层完全一致,则返回true

const equal = shallow(a, b)

[!NOTE]shallow追求的是“快速比较”,使用时必须牢记它的局限性:嵌套结构的对象/数组不会被递归比较,因此嵌套内容相同但引用不同的对象会被判定为不相等(详见故障排查一节)。

类型签名

shallow<T>(a: T, b: T): boolean
  • a:参与比较的第一个值。
  • b:参与比较的第二个值。
  • 返回值:当ab基于顶层属性的浅比较结果相等时返回true,否则返回false

shallowObject.is的区别

shallow的第一个快速路径就是Object.is(见下文源码分析),但二者定位完全不同:

比较方式原始值相同引用的对象内容相同但引用不同的对象
Object.is按值比较truefalse
shallow按值比较true顶层属性相同时为true

也就是说,Object.is是“引用/值”的严格同一性判断,而shallowObject.is判等失败后,还会进一步对顶层属性逐项比较,这正是它作为状态变化检测工具的价值所在。

使用方法

比较原始类型(Primitives)

比较stringnumberbooleanBigInt等原始值时,Object.isshallow的结果完全一致:相同值返回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会逐项检查顶层属性(firstNamelastNameage)及其值,全部相同则返回true

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

需要特别强调的是:只要顶层键集合不同,哪怕值有重叠也会返回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(多了一个键)

比较 Set

shallow比较两个Set时,检查它们是否都是Set实例且包含相同元素(不要求插入顺序一致),内容一致则返回true

const 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.tsx):

shallow(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

比较 Map

shallow比较两个Map时,检查键值对是否完全一致(同样不要求插入顺序一致),一致则返回true

const 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,并且键按引用比较——两个不同的空对象作为键时结果为false

const obj = {} const obj2 = {} shallow( new Map<object, unknown>([[obj, 'foo']]), new Map<object, unknown>([[obj2, 'foo']]), ) // -> false(键的引用不同)

源码级原理:shallow的完整实现

shallow的核心实现位于 src/vanilla/shallow.ts,整个判断流程可分为五个阶段:

export function shallow<T>(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.ts):

const isIterable = (obj: object): obj is Iterable<unknown> => Symbol.iterator in obj const hasIterableEntries = ( value: Iterable<unknown>, ): value is Iterable<unknown> & { entries(): Iterable<[unknown, unknown]> } => // HACK: avoid checking entries type 'entries' in value
  • 若双方都具备entries()方法(如MapSetURLSearchParams、纯对象包装),走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;数组内嵌对象同样只做引用比较,两个内容相同的新对象数组结果为false
  • URLSearchParams(走compareEntries,键值对比较且顺序无关):shallow(new URLSearchParams({ a: 'a' }), new URLSearchParams({ a: 'a' }))true
  • 生成器/纯可迭代对象(走compareIterables,tests/vanilla/shallow.test.tsx):两个产出相同序列的生成器比较为true
  • 跨类型比较一律为false(tests/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 useShallow<S, U>(selector: (state: S) => U): (state: S) => U { const prev = React.useRef<U>(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 迁移指南中的稳定选择器输出要求)。

场景二:subscribeWithSelectorequalityFn

src/middleware/subscribeWithSelector.ts 允许对 store 的subscribe传入选择器与自定义equalityFn,默认值为Object.is

const 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的目标一致。

导出路径与包结构

shallowuseShallow通过 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/shallow'(tests/vanilla/shallow.test.tsx)印证了第一条导出路径的有效性。

故障排查

嵌套对象比较返回false,即使内容完全一致

shallow只比较顶层属性,不检查嵌套对象或深层属性,本质上是对每个顶层属性做引用比较。在下面的例子中,两个对象的address都是嵌套对象,虽然内部内容逐字相同,但各自拥有不同的引用,因此shallow判定它们不同,返回false

const 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 明确标注Dateunsupported cases(比较结果不可预期,不应依赖)。

小结

shallow是 Zustand 性能优化体系中最基础也最常用的一环:它以Object.is为快速路径,配合原型检查、可迭代对象分派(compareEntries/compareIterables)和Object.entries兜底,实现了对原始类型、普通对象、SetMap、数组、URLSearchParams、生成器等多种数据结构的顶层浅比较。牢记它的边界——嵌套结构按引用比较、不同原型直接不等——就能在useShallowsubscribeWithSelectoruseStoreWithEqualityFn中安全地用它消除无谓的重渲染与订阅回调。想深入验证其行为,可直接阅读 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询