zustand 是一个非常时髦的状态管理库,也是 2021 年 Star 增长最快的 React 状态管理库。它的理念非常函数式,API 设计的很优雅,值得学习。
首先介绍 zustand 的使用方法。
通过 create
函数创建 store,回调可拿到 get
set
就类似 Redux 的 getState
与 setState
,可以获取 store 瞬时值与修改 store。返回一个 hook 可以在 React 组件中访问 store。
import create from 'zustand'
const useStore = create((set, get) => ({
bears: 0,
increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 })
}))
上面例子是全局唯一的 store,也可以通过 createContext
方式创建多实例 store,结合 Provider 使用:
import create from 'zustand'
import createContext from 'zustand/context'
const { Provider, useStore } = createContext()
const createStore = () => create(...)
const App = () => (
...
)
通过 useStore
在组件中访问 store。与 redux 不同的是,无论普通数据还是函数都可以存在 store 里,且函数也通过 selector 语法获取。因为函数引用不可变,所以实际上下面第二个例子不会引发重渲染:
function BearCounter() {
const bears = useStore(state => state.bears)
return {bears} around here ...
}
function Controls() {
const increasePopulation = useStore(state => state.increasePopulation)
return
}
如果嫌访问变量需要调用多次 useStore
麻烦,可以自定义 compare 函数返回一个对象:
const { nuts, honey } = useStore(state => ({ nuts: state.nuts, honey: state.honey }), shallow)
利用 useCallback
甚至可以跳过普通 compare,而仅关心外部 id 值的变化,如:
const fruit = useStore(useCallback(state => state.fruits[id], [id]))
原理是 id 变化时,useCallback
返回值才会变化,而 useCallback
返回值如果不变,useStore
的 compare 函数引用对比就会为 true
,非常巧妙。
set
函数第二个参数默认为 false
,即合并值而非覆盖整个 store,所以可以利用这个特性清空 store:
const useStore = create(set => ({
salmon: 1,
tuna: 2,
deleteEverything: () => set({ }, true), // clears the entire store, actions included
}))
所有函数都支持异步,因为修改 store 并不依赖返回值,而是调用 set
,所以是否异步对数据流框架来说都一样。
还是用英文比较表意,即 subscribeWithSelector
,这个中间件可以让我们把 selector 用在 subscribe 函数上,相比于 redux 传统的 subscribe,就可以有针对性的监听了:
mport { subscribeWithSelector } from 'zustand/middleware'
const useStore = create(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })))
// Listening to selected changes, in this case when "paw" changes
const unsub2 = useStore.subscribe(state => state.paw, console.log)
// Subscribe also exposes the previous value
const unsub3 = useStore.subscribe(state => state.paw, (paw, previousPaw) => console.log(paw, previousPaw))
// Subscribe also supports an optional equality function
const unsub4 = useStore.subscribe(state => [state.paw, state.fur], console.log, { equalityFn: shallow })
// Subscribe and fire immediately
const unsub5 = useStore.subscribe(state => state.paw, console.log, { fireImmediately: true })
后面还有一些结合中间件、immer、localstorage、redux like、devtools、combime store 就不细说了,都是一些细节场景。值得一提的是,所有特性都是正交的。
其实大部分使用特性都在利用 React 语法,所以可以说 50% 的特性属于 React 通用特性,只是写在了 zustand 文档里,看上去像是 zustand 的特性,所以这个库真的挺会借力的。
任何数据流管理工具,都有一个最核心的 store 实例。对 zustand 来说,便是定义在 vanilla.ts
文件的 createStore
了。
createStore
返回一个类似 redux store 的数据管理实例,拥有四个非常常见的 API:
export type StoreApi = {
setState: SetState
getState: GetState
subscribe: Subscribe
destroy: Destroy
}
首先 getState
的实现:
const getState: GetState = () => state
就是这么简单粗暴。再看 state
,就是一个普通对象:
let state: TState
这就是数据流简单的一面,没有魔法,数据存储用一个普通对象,仅此而已。
接着看 setState
,它做了两件事,修改 state
并执行 listenser
:
const setState: SetState = (partial, replace) => {
const nextState = typeof partial === 'function' ? partial(state) : partial
if (nextState !== state) {
const previousState = state
state = replace ? (nextState as TState) : Object.assign({}, state, nextState)
listeners.forEach((listener) => listener(state, previousState))
}
}
修改 state
也非常简单,唯一重要的是 listener(state, previousState)
,那么这些 listeners
是什么时候注册和声明的呢?其实 listeners
就是一个 Set 对象:
const listeners: Set> = new Set()
注册和销毁时机分别是 subscribe
与 destroy
函数调用时,这个实现很简单、高效。对应代码就不贴了,很显然,subscribe
时注册的监听函数会作为 listener
添加到 listeners
队列中,当发生 setState
时便会被调用。
最后我们看 createStore
的定义与结尾:
function createStore(createState) {
let state: TState
const setState = /** ... */
const getState = /** ... */
/** ... */
const api = { setState, getState, subscribe, destroy }
state = createState(setState, getState, api)
return api
}
虽然这个 state
是个简单的对象,但回顾使用文档,我们可以在 create
创建 store 利用 callback 对 state 赋值,那个时候的 set
、get
、api
就是上面代码倒数第二行传入的:
import { create } from 'zustand'
const useStore = create((set, get) => ({
bears: 0,
increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 })
}))
至此,初始化 store 的所有 API 的来龙去脉就梳理清楚了,逻辑简单清晰。
上面我们说清楚了如何创建 store 实例,但这个实例是底层 API,使用文档介绍的 create
函数在 react.ts
文件定义,并调用了 createStore
创建框架无关数据流。之所 create
定义在 react.ts
,是因为返回的 useStore
是一个 Hooks,所以本身具有 React 环境特性,因此得名。
该函数第一行就调用 createStore
创建基础 store,因为对框架来说是内部 API,所以命名也叫 api:
const api: CustomStoreApi = typeof createState === 'function' ? createStore(createState) : createState
const useStore: any = (
selector: StateSelector = api.getState as any,
equalityFn: EqualityChecker = Object.is
) => /** ... */
接下来所有代码都在创建 useStore
这个函数,我们看下其内部实现:
简单来说就是利用 subscribe
监听变化,并在需要的时候强制刷新当前组件,并传入最新的 state
给到 useStore
。所以第一步当然是创建 forceUpdate
函数:
const [, forceUpdate] = useReducer((c) => c + 1, 0) as [never, () => void]
然后通过调用 API 拿到 state
并传给 selector,并调用 equalityFn
(这个函数可以被定制)判断状态是否发生了变化:
const state = api.getState()
newStateSlice = selector(state)
hasNewStateSlice = !equalityFn(
currentSliceRef.current as StateSlice,
newStateSlice
)
如果状态变化了,就更新 currentSliceRef.current
:
useIsomorphicLayoutEffect(() => {
if (hasNewStateSlice) {
currentSliceRef.current = newStateSlice as StateSlice
}
stateRef.current = state
selectorRef.current = selector
equalityFnRef.current = equalityFn
erroredRef.current = false
})
useIsomorphicLayoutEffect
是同构框架常用 API 套路,在前端环境是useLayoutEffect
,在 node 环境是useEffect
:
说明一下 currentSliceRef
与 newStateSlice
的功能。我们看 useStore
最后的返回值:
const sliceToReturn = hasNewStateSlice
? (newStateSlice as StateSlice)
: currentSliceRef.current
useDebugValue(sliceToReturn)
return sliceToReturn
发现逻辑是这样的:如果 state 变化了,则返回新的 state,否则返回旧的,这样可以保证 compare 函数判断相等时,返回对象的引用完全相同,这个是不可变数据的核心实现。另外我们也可以学习到阅读源码的技巧,即要经常跳读。
那么如何在 selector 变化时更新 store 呢?中间还有一段核心代码,调用了 subscribe
,相信你已经猜到了,下面是核心代码片段:
useIsomorphicLayoutEffect(() => {
const listener = () => {
try {
const nextState = api.getState()
const nextStateSlice = selectorRef.current(nextState)
if (!equalityFnRef.current(currentSliceRef.current as StateSlice, nextStateSlice)) {
stateRef.current = nextState
currentSliceRef.current = nextStateSlice
forceUpdate()
}
} catch (error) {
erroredRef.current = true
forceUpdate()
}
}
const unsubscribe = api.subscribe(listener)
if (api.getState() !== stateBeforeSubscriptionRef.current) {
listener() // state has changed before subscription
}
return unsubscribe
}, [])
这段代码要先从 api.subscribe(listener)
看,这使得任何 setState
都会触发 listener
的执行,而 listener
利用 api.getState()
拿到最新 state
,并拿到上一次的 compare 函数 equalityFnRef
执行一下判断值前后是否发生了改变,如果改变则更新 currentSliceRef
并进行一次强制刷新(调用 forceUpdate
)。
注意到 context 语法,可以创建多个互不干扰的 store 实例:
import create from 'zustand'
import createContext from 'zustand/context'
const { Provider, useStore } = createContext()
const createStore = () => create(...)
const App = () => (
...
)
首先我们知道 create
创建的 store 是实例间互不干扰的,问题是 create
返回的 useStore
只有一个实例,也没有
声明作用域,那么如何构造上面的 API 呢?
首先 Provider
存储了 create
返回的 useStore
:
const storeRef = useRef()
storeRef.current = createStore()
那么 useStore
本身其实并不实现数据流功能,而是将
提供的 storeRef
拿到并返回:
const useStore: UseContextStore = (
selector?: StateSelector,
equalityFn = Object.is
) => {
const useProviderStore = useContext(ZustandContext)
return useProviderStore(
selector as StateSelector,
equalityFn
)
}
所以核心逻辑还是是现在 create
函数里,context.ts
只是利用 ReactContext 将 useStore
“注入” 到组件,且利用 ReactContext 特性,这个注入可以存在多个实例,且不会相互影响。
中间件其实不需要怎么实现。比如看这个 redux 中间件的例子:
import { redux } from 'zustand/middleware'
const useStore = create(redux(reducer, initialState))
可以将 zustand 用法改变为 reducer,实际上是利用了函数式理念,redux 函数本身可以拿到 set, get, api
,如果想保持 API 不变,则原样返回 callback 就行了,如果想改变用法,则返回特定的结构,就是这么简单。
为了加深理解,我们看看 redux 中间件源码:
export const redux = ( reducer, initial ) => ( set, get, api ) => {
api.dispatch = action => {
set(state => reducer(state, action), false, action)
return action
}
api.dispatchFromDevtools = true
return { dispatch: (...a) => api.dispatch(...a), ...initial }
}
将 set, get, api
封装为 redux API:dispatch
本质就是调用 set
。
zustand 是一个实现精巧的 React 数据流管理工具,自身框架无关的分层合理,中间件实现巧妙,值得学习。
讨论地址是:精读《zustand 源码》· Issue #392 · dt-fe/weekly
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)