关于前端:精读zustand-源码

45次阅读

共计 7768 个字符,预计需要花费 20 分钟才能阅读完成。

zustand 是一个十分时尚的状态治理库,也是 2021 年 Star 增长最快的 React 状态治理库。它的理念十分函数式,API 设计的很优雅,值得学习。

概述

首先介绍 zustand 的应用办法。

创立 store

通过 create 函数创立 store,回调可拿到 get set 就相似 Redux 的 getStatesetState,能够获取 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()

注册和销毁机会别离是 subscribedestroy 函数调用时,这个实现很简略、高效。对应代码就不贴了,很显然,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 赋值,那个时候的 setgetapi 就是下面代码倒数第二行传入的:

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

阐明一下 currentSliceRefnewStateSlice 的性能。咱们看 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 许可证)

正文完
 0