浅析 zustand 状态管理器
zustand 是轻量型、快速的状态管理工具,而且使用很简单,也解决了很多常见的问题,比如可怕的 zombie child 问题、react concurrency,以及混合渲染器之间的上下文丢失。
npm install zustand
使用
这 part 不会多说,因为官网文档都写的很清楚。
创建一个 store
import create from 'zustand'
export const useStore = create((set, get) => ({ todoList: [ { date: 1671523153045, content: '起床刷牙', }, { date: 1671523153046, content: '去旅游', }, ],}))
读取store hooks
在任何地方使用这个 hook,不需要 provider。组件会在你选择的状态变化时重新渲染。
import { useStore } from 'store'
function App() { const todoList = useStore(state => state.todoList)
return <div> { todoList.map(item => ( <div key={item.date}>{item.content}</div> )) } </div>}
actions
zustand 不关心你的 action 是否是异步。你只需要在恰当的时候调用 set
即可。(你可以理解为就是一个处理函数)
import create from 'zustand'
export const useStore = create((set, get) => ({ // 省略部分代码 // 异步 action insert: async (content) => { const todo = { date: Date.now(), content, } await fetchInsert() // 这里为了模拟异步,就是简单的 setTimeout // set() 支持接收一个函数或状态对象 set((state) => ({ todoList: [todo, ...state.todoList] })) }, // 同步 action delete: (key) => { // get() 读取外部 状态, 就类似 redux reducer 中的 state const prevTodoList = get().todoList const targetIndex = prevTodoList.findIndex((todo) => todo.date === key) if (targetIndex < 0) return prevTodoList.splice(targetIndex, 1) set({ todoList: [...prevTodoList] }) },}))
// 使用const insert = useStore(state => state.insert)insert('actions')
如果你觉得离不开 redux 中的 reducers
和 action types
,也可以写成这样👇🏻
import create from 'zustand'
const types = { insert: 'INSERT_TODO', delete: 'DELETE_TODO' }
const reducer = (state, { type, payload } => { switch (type) { case types.insert: const todo = { date: Date.now(), content: payload.content, }
return { todoList: [todo, ...state.todoList] } case typs.delete: const prevTodoList = get().todoList const targetIndex = prevTodoList.findIndex((todo) => todo.date === payload.key) if (targetIndex < 0) return prevTodoList prevTodoList.splice(targetIndex, 1) return { todoList: [...prevTodoList] } }})
const useStore = create(set => ({ todoList: [], dispatch: (payload) => set(state => reducer(state, payload))}))
// 使用const dispatch = useStore(state => state.dispatch)dispatch({ type: types.insert, payload: { content: 'mock redux' } })
到此为止,已经可以简单使用 zustand了。
多状态
在上面的例子中,都是使用这种读取方式(单个状态),它基于严格相等来检测变化 old === new
const todoList = useStore(state => state.todoList)const insert = useStore(state => state.insert)
如果我们想要一次获取多个状态,类似 mapStateToProps
时,可以采取这种方式👇🏻
import shallow from 'zustand/shallow' // 内置的比较函数(浅层diff)
const [ todoList, insertTodo, deleteTodo ] = useStore(state => [state.todoList, state.insert, state.delete], shallow) // state.todoList, state.insert, state.delete 改变时,重新渲染组件// orconst { todoList, insertTodo, deleteTodo } = useStore(state => {state.todoList, state.insert, state.delete}, shallow)
提示
获取所有状态 const state = useStore()
,但不建议使用,因为任意 state
修改,都会导致组件重新渲染。
覆盖状态
set
有第二个参数,默认值为 false
,即进行合并操作;若为 true
则为覆盖操作
import create from 'zustand'
export const useStore = create((set, get) => ({ // 省略部分代码 destoryApp: () => set({}, true), // 这一步会将 store 覆盖为 {}}))
在没有 React 情况下使用
也就是在外部 js
文件下使用 zustand。
// 创建 store,与前面案例一致export const useStore = create( // ...)
// useStore 有 4 个属性,分别为 getState, setState, subscribe, destroy
// getState 获取 state,与 create 中的 get 一致const todoList = useStore.getState().todoList
// setState 设置 state,与 create 中的 set 一致useStore.setState({ todoList: []})
// subscribe 订阅器,每当 state 发生变化时都会触发useStore.subscribe(() => { console.log('触发了')})
// destroy 销毁store,删除所有订阅useStore.destroy()
// 取消订阅useStore()
完整 ToDoList 案例代码 更多高级用法自行查阅官方仓库
源码解析
插叙
如果想要学习一下 rollup
打包 和 swc
的话也可以看看 zustand
源码库,它里面也有使用到,而且都比较简单,容易入门。
swc 是基于 Rust
实现的编译工具,它可以实现 babel
的功能和 typescript
编译,并且速度更快。同时它也推出 swcpack
打包工具,但目前问题还是比较多,更多只是用来代替 babel
。
zustand 的源码主要是涉及 src/react.ts
和 src/vanilla.ts
这两个文件。
中间件的源代码放在 src/middleware
,有兴趣可以阅读一下,在一 part 不会讲它。😬
zustand 也提供了丰富的测试文件 tests
,可以很方便的进行调试。
回到正题:zustand 状态管理库主要思想就是利用 发布订阅模式 和 use-sync-external-store
(本质是 React.useSyncExternalStore
的单独一个包,用于正确订阅储存中的值,解决并发渲染导致的 Tearing 问题,即 React concurrent
)
🔍 这一段就是创建 store( create
函数) 的核心代码👇🏻
const createStoreImpl = (createState) => { let state const listeners = new Set() // 储存订阅者
/** * @description: 设置 state,等价于 set * @param {funtion|object} partial 更新后的state 或 更新函数(state) => set(...) * @param {boolean} replace true 为覆盖,默认false 合并 * @return {void} */ 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 // 覆盖 : Object.assign({}, state, nextState) // 合并 listeners.forEach((listener) => listener(state, previousState)) // 发布事件(即通知更新),参数主要给我们自定义订阅事件使用,react 的 listenr (后面有讲这个怎么来)并没有用到 } }
// 返回 state,等价于 get const getState = () => state
/** * @description: 监听器 * @param {StoreApi<TState>['subscribe']} listener (state, prevState) => void * @return {function} 返回取消订阅函数 */ const subscribe = (listener) => { listeners.add(listener) // 返回销毁监听器函数 return () => listeners.delete(listener) }
/** * @description: 销毁(清空)监听器 函数 * @return {void} */ const destroy = () => listeners.clear() const api = { setState, getState, subscribe, destroy }
// 这一步就是你 create 传入的函数,如 const useStore = create((set, get) => ({})) state = createState(setState, getState, api) return api}
🏷️ 倘若我们有一段这样的代码,就好理解很多了。
export const useStore = createStoreImpl((set, get) => ({ todoList: [], insert: () => {}}))
useStore
对应就是函数的返回,即 api = { setState, getState, subscribe, destroy }
。
createState
对应的就是创建 state
函数,即 (set, get) => ({})
。
state
对应的就是 creaetState
的返回值 {...}
。
至此,在 没有 React 情况下使用 的功能已经完成,主要的 listener
是通过 subscribe
进行绑定的,然而在 React
下是如何触发更新的呢?
🤔 如何在react 中正确的订阅外部储存的值?主要就是利用到 useSyncExternalStoreWithSelector
,它是 useSyncExternalStore
指定选择器优化版。
React 18 - 了解 useSyncExternalStore 我们都知道 React Fiber 调度是在 16版本后采用的,它将复杂任务进行分片,优先调度高优先级,调度过程中可以挂起、恢复、终止。因此会进行并发渲染,就导致了一个问题,比如有 A、B、C三个节点,都是渲染同一个外部存储 state,但渲染完 A 时,React 暂停了当前任务调度,将外部存储 state 修改了,当 React 恢复渲染的时候,B、C都渲染为新值,而A依旧是旧值。 React 为了解决这个问题加入了
useMutableSource
,后面重新设计为useSyncExternaStore
。
✏️ 先使用一个简单的案例,理解 useSyncExternaStore
,store
设计很简单,重点在 useStore
。
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports
const store = { state: { data: 0 }, listeners: [], subscribe(l) { store.listeners.push(l) }, getState() { return store.state }, setState(partial) { const nextState = partial(store.state) store.state = Object.assign({}, store.state, nextState) // 每当值修改的时候,就会触发所有订阅器,达到组件更新(因为在调用useSyncExternalStoreWithSelector 时 react 已经往 subscribe 注入了一个 listener) store.listeners.forEach((listener) => listener()) }, add() { store.setState(state => ({ data: state.data + 1 })) }}
// 前身 useMutableSource// useSyncExternalStoreWithSelector 用于读取和订阅外部数据源的 hook,与 useSyncExternalStore 类似// 参数一:用于注册一个回调函数,当存储值发生更改时被调用,react会传入一个listener,当数据发生改变时必须调用这个listener// 参数二:返回当前存储值的函数,即返回 state 的函数。// 参数三:返回服务端渲染期间使用的存储值的函数。// 参数四:选择返回指定状态的 seletor 函数,利用参数二其实也能达到效果,但获取不同的值都要修改参数二会比较麻烦。// 参数五:对比函数,决定是否更新const useStore = (selector, equalityFn) => useSyncExternalStoreWithSelector( store.subscribe, store.getState, store.getState, selector, equalityFn)
function App() { // selector = state => state.data const count = useStore(state => state.data)
return <div> <div>count:{count}</div> <div> <button onClick={() => store.add()}>add+</button> </div> </div>}
export default App
React 中能正确订阅且触发更新的核心代码👇🏻
api
对应 createStoreImpl
返回值,即 {setState, getState, subscribe, destroy}
。
selector
对应使用 store
传入的函数,如 useStore(state => state.data)
。
也就是不建议为空,获取所有数据,只要 任意数据一改变就会导致频繁触发更新。
/** * @description: 返回对应 state 的值 * @param {{ setState, getState, subscribe, destroy }} api * @param {(snapshot: Snapshot) => Selection} selector 选择器 * @param {(a, b) => boolean} equalityFn 对比函数,是否更新 * @return {any} */export function useStore( api, selector = (state) => api.getState, equalityFn) { const slice = useSyncExternalStoreWithSelector( api.subscribe, api.getState, api.getServerState || api.getState, selector, equalityFn )
// react hooks (不重要) useDebugValue(slice) return slice}
zustand 的核心代码告一段落了,这个库代码量不多,设计简单,但不可否认是一个优秀状态管理解决方案。
最后这段只是简单的导出 create
函数,用于理解整个流程。
将 createStoreImpl
的返回值与 useStore
的建立函数合并 构成 createImpl
(完整 create
函数) 的返回值。
// 创建接口(createState 依旧是那个创建 state 函数)const createImpl = (createState) => { // api = { setState, getState, subscribe, destroy } // createStore 对应的就是 create 核心,也就是上面的 createStoreImpl const api = typeof createState === 'function' ? createStore(createState) : createState
const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn)
Object.assign(useBoundStore, api) return useBoundStore}
// 真正的 create 函数const create = (createState) => createState ? createImpl(createState) : createImpl