Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

zustand 源码分析

作者:oasis


前置阅读:

zustand 简介

zustand(德语”状态”的意思)是一个轻量级、快速且可扩展的状态管理库。它基于简化的 Flux 原则,提供了基于 Hook 的简洁 API,代码量极小(压缩后不到 1KB),同时避免了传统状态管理方案的复杂性。

核心特点:

相比 Redux,zustand 减少了大量模板代码;相比 Context API,它提供了更细粒度的更新控制和更好的性能表现。

zustand 基础用法

使用 zustand 非常简单:通过 create 函数创建一个 store hook。create 接收一个状态创建函数,该函数接收 setgetStateapi 三个参数,返回初始状态对象和更新方法。

创建好的 hook 可以直接在组件中使用,无需任何 Provider 包裹。hook 支持传入一个可选的 selector 函数来选择状态中的特定部分,这样可以实现精确订阅:只有当 selector 选中的状态发生变化时,组件才会重新渲染,避免了不必要的性能开销。如果不传 selector,则返回整个状态对象(注意这会导致状态任何变化都会触发重渲染)。

import { create } from 'zustand'

const useBear = create((set) => ({
    bears: 0,
    increase: () => set((state) => ({bears: state.bears + 1}))
}))

const App = () => {
    const bears = useBear((state) => state.bears)

    return <div>{bears}</div>
}

zustand 的 create 方法执行过程

在前面的例子中,create 接收了一个箭头函数,该函数接收一个名为 set 的参数,调用 set 可以触发状态的变更。

const create = (createState) => createState ? createImpl(createState) : createImpl

当调用 create 并传入状态创建函数时,会执行 createImpl(createState)createImpl 的实现如下:

const createImpl = (createState) => {
    const api = createStore(createState)

    const useBoundStore = (selector) => useStore(api, selector)

    Object.assign(useBoundStore, api)

    return useBoundStore
}

createImpl 首先调用 createStore 创建 store API,然后创建一个 useBoundStore 函数,该函数可以接收一个可选的 selector 参数。最后通过 Object.assign 将 store API 的方法(setStategetStatesubscribegetInitialState)都挂载到 useBoundStore 上,这样我们就可以直接通过 hook 访问这些方法了。

createStore 的实现

createImpl 内部会调用 createStore 来创建 store。createStore 的核心实现是 createStoreImpl,它负责创建状态存储的核心逻辑:

function createStoreImpl(createState) {
    let state;
    const listeners = new Set()

    // 用于触发状态更新
    const setState = (partial, replace) => {
        const nextState = typeof partial === 'function' 
            ? partial(state) 
            : partial
        if (!Object.is(nextState, state)) {
            const previousState = state
            state = replace ?? (typeof nextState !== 'object' || nextState === null)
                ? nextState
                : Object.assign({}, state, nextState)
            listeners.forEach((listener) => listener(state, previousState))
        }
    }

    const getState = () => state

    const subscribe = (listener) => {
        listeners.add(listener)
        // Unsubscribe
        return () => listeners.delete(listener)
    }

    const api = {setState, getState, subscribe}

    const initialState = (state = createState(setState, getState, api))

    const getInitialState = () => initialState

    return { setState, getState, subscribe, getInitialState }
}

createStoreImpl 执行时,createState 函数会被调用,传入 setStategetStateapi 作为参数。createState 函数会返回初始状态对象,这个对象会被赋值给 state 变量,同时也被保存为 initialState。代换后,代码如下:

// createState 被调用,返回初始状态
const initialState = (state = createState(setState, getState, api))
// 等价于:
const initialState = (state = {
    bears: 0,
    increase: () => setState((state) => ({bears: state.bears + 1}))
})
// 此时 state 和 initialState 都指向同一个对象

接下来需要将状态和 React 连接起来,zustand 采用了 React.useSyncExternalStore 来实现这一功能。

useStore 函数中,会调用 React.useSyncExternalStore

const slice = React.useSyncExternalStore(
    api.subscribe,
    () => selector(api.getState()),
    () => selector(api.getInitialState())
)

create 创建的 hook 可以接收一个 selector 函数,用来从 state 中选择需要的切片。这个 selector 函数会被应用到 api.getState()api.getInitialState() 的返回值上,从而只订阅状态中相关的部分。

接下来我们看看更新操作是如何触发的。当我们调用 increase 方法时,会执行 set((state) => ({bears: state.bears + 1}))

这里的 set 函数实际上是 setState。在 setState 中,会计算新的状态,如果状态发生了变化(通过 Object.is 比较),就会遍历所有的 listeners 并调用它们,通知订阅者状态已更新。由于 React.useSyncExternalStore 订阅了 api.subscribe,当 listeners 被触发时,React 就会重新渲染组件,从而更新 UI。

总结

通过分析 zustand 的源码,我们可以看到它之所以能够成为 React 生态中备受欢迎的状态管理方案,主要得益于以下几个方面的设计:

核心架构设计

  1. 发布-订阅模式:zustand 的核心是基于发布-订阅模式的状态管理。createStoreImpl 维护了一个 listeners Set,当状态更新时,会通知所有订阅者。这种设计简单高效,避免了复杂的中间层。

  2. React 官方 API 集成:使用 React.useSyncExternalStore 作为 React 和外部状态存储之间的桥梁,这是 React 18+ 官方推荐的方式。它不仅解决了并发渲染的问题,还正确处理了 zombie child、context loss 等边界情况。

  3. 精确的状态订阅:通过 selector 函数,组件可以只订阅状态中需要的部分。配合 Object.is 进行严格相等比较,确保只有在相关状态真正变化时才触发重渲染,实现了细粒度的性能优化。

关键实现细节

zustand 的成功在于它在简洁性和功能性之间找到了完美的平衡。它没有过度设计,而是专注于解决状态管理的核心问题,通过巧妙的设计和 React 官方 API 的配合,实现了既简单又强大的状态管理方案。