zustand使用和源码分析

243 阅读4分钟

什么是zustand

zustand是一种小型快的的状态管理框架,拥有基于hook的舒适小巧的API,对比Mobx和Redux,它更小巧,学习曲线更平稳。

git地址:github.com/pmndrs/zust…

基础用法

1.创建store

import { create } from 'zustand'

//数据State类型
export interface TestState {
    data: number; //数据类型
    asyncIncrease: (by: number) => Promise<void>; //修改数据的异步方法
    increase: (by: number) => void; //修改数据的同步方法
    removeAll: () => void;
}

//创建返回数据对象的hook
export const useTestState = create<TestState>()((set, getState, store) => ({
    data: 1,
    asyncIncrease: async (by: number) => {
        await set((state) => ({ data: state.data + by }))
    },
    increase: (by) => set((state) => ({ data: state.data + by })),
    removeAll: () => set({ data: 0 })
}));

先创建一个State类型进行声明,然后调用zustand的create方法创建一个hook方法,该方法可以返回state对象和修改state对象的action(incrase,asyncIncrease等方法),其中create()传入一个函数,该函数类型接收三个参数并且返回一个泛型定义的State对象(TestState),第一个参数set也是一个函数,可以接收state对象或者一个入参为state,返回值为state的函数,在这里可以对state进行修改,然后触发组件的重新渲染。注意这里采用的是属性合并的模式,所以set调用里只需要对有变化的属性修改返回即可。

2.组件使用

import React, { type VFC, memo } from 'react';
import {Text, TouchableOpacity, View } from 'react-native';
import { type AppInitialProps } from '@mfe/plus';
import { useStyles } from './styles';
import { useTestState } from '@/store/appStore';

const App: VFC<AppInitialProps> = () => {
  //调用state hook方法
  const { data, increase, removeAll } = useTestState();
  // const increase = useTestState((state) => state.increase);
  const styles = useStyles();


  return (
    <View style={styles.container}>
      <TouchableOpacity onPress={() => { removeAll() }}> //调用state进行方法更新。
        <Text>{`data的数量是${data}`}</Text> //引用state
      </TouchableOpacity>
    </View>
  );
};
export default memo<typeof App>(App);

以上就是完整的zustand的使用方法,非常简洁,不需要useState,不需要useEffect,也不需要进行相应的依赖关心。

3.源码分析

useSyncExternalStore分析

先介绍一下一个在react 18新增的一个hook api

    export function useSyncExternalStore<Snapshot>(
        subscribe: (onStoreChange: () => void) => () => void,
        getSnapshot: () => Snapshot,
        getServerSnapshot?: () => Snapshot,
    ): Snapshot;

它的作用就是允许你订阅一个外部存储State,并且在外部存储State更新时,触发组件的刷新。它的意义是什么?举个例子

import React from 'react';
import { Text, View,TouchableOpacity } from 'react-native';

const store = {
    currentState:{data:0},
    listeners:[],
    reducer(action){
        switch(action.type) {
            case 'ADD':
                return {data:store.currentState.data+1}
            default:
                return store.state
        }
    },
    subscribe(l){
        store.listeners.push(l)
    },
    getSnapshot() {
        return store.currentState
    },
    dispatch(action) {
        store.currentState = store.reducer(action)
        store.listeners.forEach(l=>l())
        return action;
    }
}




const YourApp: VFC<AppInitialProps> = () => {

  const outerState = React.useSyncExternalStore(store.subscribe, ()=>store.getSnapshot().data);
  console.log(`outState is ${outerState}`);


  return (
    <View style={{flex:1}}>
      <TouchableOpacity onPress={() => { 
   store.dispatch({type:'ADD'})}}>
        <Text>{`data的数量是${JSON.stringify(outerState)}`}</Text>
      </TouchableOpacity>
    </View>
  );
};


export default YourApp;

我自己定义了一个store,它内部进行自己状态的管理,并且定义了一系列行为,我在组件内部使用的时候,只需要使用useSyncExternalStore将对store里暴露的两个方法关联进去即可,后续我们直接使用outerState里面的数据和行为进行展示和刷新。

通过上面的代码你会发现,页面的刷新除了useSyncExternalStore其实你并没有使用类似useState和useEffect等hook,这就是它的价值所在,他为我们提供了一种store的管理维护方法,并且不用使用任何内置hook。

看一下useSyncExternalStore的源码

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import * as React from 'react';
import is from 'shared/objectIs';

// Intentionally not using named imports because Rollup uses dynamic
// dispatch for CommonJS interop named imports.
const {useState, useEffect, useLayoutEffect, useDebugValue} = React;

let didWarnOld18Alpha = false;
let didWarnUncachedGetSnapshot = false;

// Disclaimer: This shim breaks many of the rules of React, and only works
// because of a very particular set of implementation details and assumptions
// -- change any one of them and it will break. The most important assumption
// is that updates are always synchronous, because concurrent rendering is
// only available in versions of React that also have a built-in
// useSyncExternalStore API. And we only use this shim when the built-in API
// does not exist.
//
// Do not assume that the clever hacks used by this hook also work in general.
// The point of this shim is to replace the need for hacks by other libraries.
export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  // Note: The shim does not use getServerSnapshot, because pre-18 versions of
  // React do not expose a way to check if we're hydrating. So users of the shim
  // will need to track that themselves and return the correct value
  // from `getSnapshot`.
  getServerSnapshot?: () => T,
): T {


  // Read the current snapshot from the store on every render. Again, this
  // breaks the rules of React, and only works here because of specific
  // implementation details, most importantly that updates are
  // always synchronous.
  const value = getSnapshot();

  // Because updates are synchronous, we don't queue them. Instead we force a
  // re-render whenever the subscribed state changes by updating an some
  // arbitrary useState hook. Then, during render, we call getSnapshot to read
  // the current value.
  //
  // Because we don't actually use the state returned by the useState hook, we
  // can save a bit of memory by storing other stuff in that slot.
  //
  // To implement the early bailout, we need to track some things on a mutable
  // object. Usually, we would put that in a useRef hook, but we can stash it in
  // our useState hook instead.
  //
  // To force a re-render, we call forceUpdate({inst}). That works because the
  // new object always fails an equality check.
  const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});

  // Track the latest getSnapshot function with a ref. This needs to be updated
  // in the layout phase so we can access it during the tearing check that
  // happens on subscribe.
  useLayoutEffect(() => {
    inst.value = value;
    inst.getSnapshot = getSnapshot;

    // Whenever getSnapshot or subscribe changes, we need to check in the
    // commit phase if there was an interleaved mutation. In concurrent mode
    // this can happen all the time, but even in synchronous mode, an earlier
    // effect may have mutated the store.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
  }, [subscribe, value, getSnapshot]);

  useEffect(() => {
    // Check for changes right before subscribing. Subsequent changes will be
    // detected in the subscription handler.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
    const handleStoreChange = () => {
      // TODO: Because there is no cross-renderer API for batching updates, it's
      // up to the consumer of this library to wrap their subscription event
      // with unstable_batchedUpdates. Should we try to detect when this isn't
      // the case and print a warning in development?

      // The store changed. Check if the snapshot changed since the last time we
      // read from the store.
      if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceUpdate({inst});
      }
    };
    // Subscribe to the store and return a clean-up function.
    return subscribe(handleStoreChange);
  }, [subscribe]);

  return value;
}

function checkIfSnapshotChanged<T>(inst: {
  value: T,
  getSnapshot: () => T,
}): boolean {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    return !is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}

zustand源码分析

首先看仓库的创建

export interface StoreApi<T> {
  setState: SetStateInternal<T>
  getState: () => T
  getInitialState: () => T
  subscribe: (listener: (state: T, prevState: T) => void) => () => void
}


export type StateCreator<
  T,
  Mis extends [StoreMutatorIdentifier, unknown][] = [],
  Mos extends [StoreMutatorIdentifier, unknown][] = [],
  U = T,
> = ((
  setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
  getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
  store: Mutate<StoreApi<T>, Mis>,
) => U) & { $$storeMutators?: Mos }



const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  const api = createStore(createState)

  const useBoundStore: any = (selector?: any) => useStore(api, selector)

  Object.assign(useBoundStore, api)

  return useBoundStore
}

export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
  createState ? createImpl(createState) : createImpl) as Create

首先看create方法的入参,它的入参是StateCreator,看它的定义,它是一个函数,函数的入参有三个参数,setState对应

StoreApi中的setState,类型是SetStateInternal,getState对应StoreApi中的getState函数,store对应StoreApi实例对象。接着他会调用createImpl方法,首先调用createStore方法。

const createStoreImpl: CreateStoreImpl = (createState) => {
  type TState = ReturnType<typeof createState>
  type Listener = (state: TState, prevState: TState) => void
  let state: TState
  const listeners: Set<Listener> = new Set()

  const setState: StoreApi<TState>['setState'] = (partial, replace) => {
    // TODO: Remove type assertion once https://github.com/microsoft/TypeScript/issues/37663 is resolved
    // https://github.com/microsoft/TypeScript/issues/37663#issuecomment-759728342
    const nextState =
      typeof partial === 'function'
        ? (partial as (state: TState) => TState)(state)
        : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state =
        (replace ?? (typeof nextState !== 'object' || nextState === null))
          ? (nextState as TState)
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  const getState: StoreApi<TState>['getState'] = () => state

  const getInitialState: StoreApi<TState>['getInitialState'] = () =>
    initialState

  const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
    listeners.add(listener)
    // Unsubscribe
    return () => listeners.delete(listener)
  }

  const api = { setState, getState, getInitialState, subscribe }
  const initialState = (state = createState(setState, getState, api))
  return api as any
}

这里其实是用来构建一个StoreApi,同时也为使用useSyncExternalStore做好准备,首先看下setState方法,里面会判断设置的state和原来的state会不会一致,不一致的话就会触发listener set里listener方法的调用,这个listener,就是subscribe函数里的入参函数,其实就是useSyncExternalStore传入的handleStoreChange函数,前面也分析过,当状态改变,调用handleStoreChange,就会触发重绘。

const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  const api = createStore(createState)

  const useBoundStore: any = (selector?: any) => useStore(api, selector)

  Object.assign(useBoundStore, api)

  return useBoundStore
}

createImpl接下来就是构建一个useBounStore,它是一个函数,在它的内部会调用useStore,而useStore,其实就是前面说的对useSyncExternalStore调用。

export function useStore<TState, StateSlice>(
  api: ReadonlyStoreApi<TState>,
  selector: (state: TState) => StateSlice = identity as any,
) {
  const slice = React.useSyncExternalStore(
    api.subscribe,
    () => selector(api.getState()),
    () => selector(api.getInitialState()),
  )
  React.useDebugValue(slice)
  return slice
}

所以当我们create后会返回一个useBoundStore这个hook方法,再调用后就可以通过useSyncExternalStore拿到我们的对象。并且在调用StateCreator入参的setState方法时,触发更新。

最佳实践

  1. store结构

   kim image

如果页面叫为简单单一,可以只建立一个store,使用全局store处理。如果业务较为复杂,可以从store使用范围上,store分为三类,app级别的store,page(页面)级别的store,以及component(组件)级别的store,同级之间的store不允许使用,上层的store允许使用下层的store,如果有同级别的状态需要通信,将状态下沉,组件到页面,页面到app。如上图。

  1. store的拆分

同级别的store,不宜过于庞大,可以拆分为不同的slic,然后合并处理

export interface StateA {
    data: number;
    asyncIncrease: (by: number) => Promise<void>;
    increase: (by: number) => void;
    removeAll: () => void;
}

export interface StateB {
    num: number;
}
//设置状态b的slice
const createStateBSlice: StateCreator<StateA & StateB, [], [], StateB> = (set) => ({
    num: 0
});

//设置状态A的slice
const createStateASlic: StateCreator<StateA & StateB, [], [], StateA> = (set) => ({
    data: 1,
    asyncIncrease: async (by: number) => {
        await set((state) => ({ data: state.data + by }));
    },
    increase: (by) => set((state) => { return { data: state.data + by } }),
    removeAll: () => set({ data: 0 })
});

//合并两个slice为一个store
const useStore = create<StateA & StateB>()((...a)=>({
    ...createStateASlic(...a),
    ...createStateBSlice(...a)
}))