zustand 是一个十分时尚的状态治理库,也是 2021 年 Star 增长最快的 React 状态治理库。它的理念十分函数式,API 设计的很优雅,值得学习。
概述
首先介绍 zustand 的应用办法。
创立 store
通过 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 = () => (<Provider createStore={createStore}>
...
</Provider>
)
拜访 store
通过 useStore
在组件中拜访 store。与 redux 不同的是,无论一般数据还是函数都能够存在 store 里,且函数也通过 selector 语法获取。因为函数援用不可变,所以实际上上面第二个例子不会引发重渲染:
function BearCounter() {const bears = useStore(state => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {const increasePopulation = useStore(state => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
如果嫌拜访变量须要调用屡次 useStore
麻烦,能够自定义 compare 函数返回一个对象:
const {nuts, honey} = useStore(state => ({ nuts: state.nuts, honey: state.honey}), shallow)
细粒度 memo
利用 useCallback
甚至能够跳过一般 compare,而仅关怀内部 id 值的变动,如:
const fruit = useStore(useCallback(state => state.fruits[id], [id]))
原理是 id 变动时,useCallback
返回值才会变动,而 useCallback
返回值如果不变,useStore
的 compare 函数援用比照就会为 true
,十分奇妙。
set 合并与笼罩
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 实例
任何数据流管理工具,都有一个最外围的 store 实例。对 zustand 来说,便是定义在 vanilla.ts
文件的 createStore
了。
createStore
返回一个相似 redux store 的数据管理实例,领有四个十分常见的 API:
export type StoreApi<T extends State> = {
setState: SetState<T>
getState: GetState<T>
subscribe: Subscribe<T>
destroy: Destroy
}
首先 getState
的实现:
const getState: GetState<TState> = () => state
就是这么简略粗犷。再看 state
,就是一个一般对象:
let state: TState
这就是数据流简略的一面,没有魔法,数据存储用一个一般对象,仅此而已。
接着看 setState
,它做了两件事,批改 state
并执行 listenser
:
const setState: SetState<TState> = (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<StateListener<TState>> = 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 的前因后果就梳理分明了,逻辑简略清晰。
create 函数的实现
下面咱们说分明了如何创立 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 = <StateSlice>(
selector: StateSelector<TState, StateSlice> = api.getState as any,
equalityFn: EqualityChecker<StateSlice> = 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 的实现
留神到 context 语法,能够创立多个互不烦扰的 store 实例:
import create from 'zustand'
import createContext from 'zustand/context'
const {Provider, useStore} = createContext()
const createStore = () => create(...)
const App = () => (<Provider createStore={createStore}>
...
</Provider>
)
首先咱们晓得 create
创立的 store 是实例间互不烦扰的,问题是 create
返回的 useStore
只有一个实例,也没有 <Provider>
申明作用域,那么如何结构下面的 API 呢?
首先 Provider
存储了 create
返回的 useStore
:
const storeRef = useRef<TUseBoundStore>()
storeRef.current = createStore()
那么 useStore
自身其实并不实现数据流性能,而是将 <Provider>
提供的 storeRef
拿到并返回:
const useStore: UseContextStore<TState> = <StateSlice>(
selector?: StateSelector<TState, StateSlice>,
equalityFn = Object.is
) => {const useProviderStore = useContext(ZustandContext)
return useProviderStore(
selector as StateSelector<TState, StateSlice>,
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
如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。
关注 前端精读微信公众号
<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>
版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)