一问一世界:zustand源码

923 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

1. zustandapi介绍

一个简单的类flux的数据管理库。为什么说是类flux,因为zustand的核心概念 单一数据来源(one source of truth) 是跟flux的数据概念相似的。这里有flux概念介绍

zustand是有基于hooks实现的api,但并不是所有api都是基于hooks,这也是为什么可以不依赖react去单独使用。但是zustandreact一起使用时极其舒适。

主要api就一个:

  • create: 构建store
    const useStore = create((set, get) => ({
      bears: 0,
      increase: () => set(state => ({ bears: state.bears + 1 })),
      removeAllBears: () => set({ bears: 0 })
    }))

然后可以这样使用useStore

function BearCounter() {
  const bears = useStore(state => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  const increase = useStore(state => state.increase)
  return <button onClick={increasePopulation}>one up</button>
}

2. 源码分析

如果从入口文件开始,一个函数一个函数的分析也可以。但是我比较喜欢带着问题去找答案,所以给自己提问,然后去源码中找答案。

2.1 从 create 开始

2.1.1 问题 1: 入口函数create构建的时候,接受了一个函数(createState)作为参数,签名如下,这里为什么set可以修改状态,而get可以获取所有状态,api可以用来干嘛?

// createState
(set, get, api) => { // ... }

create这里会根据当前传入的是否为函数来决定是调用系统默认的createStore函数直接使用自定义createStore

// https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts

const createStore = (createState) => {
    let state
    const listeners = new Set()
    const setState = () => {}
    const setState = () => {}
    const subscribe = () => {}
    const destroy = () => {}
    const api = { setState, getState, subscribe, destroy }
    // 传入的函数会在这里调用,同时传入set,get,api作为参数
    state = createState(set, get, api)
    return api
}
const setState = (partial, replace) => {
    // partial可以直接传入新的状态,也可以是函数的返回值
    const nextState = typeof partial === 'function' ? partial(state) : partial;
    // 浅比较,如果不同再做更新
    if (nextState !== state) {
    // 如果是replace就直接替换,否则混入到之前的状态并用新状态替换之前的状态
      state = replace ? nextState : Object.assign({}, state, nextState);
      // 调用所有监听器。(这里listen是在subscribe中注册的。)
      listeners.forEach(listener => listener(state));
    }
};
// 利用闭包,保存state状态,get时直接返回
const getState = () => state;

api中暴露了状态里的主要方式。

2.1.2 答案: 主要是利用闭包:维护一个内部状态state,set去update,get则是返回state

2.2 create()

2.2.1 问题2:create()调用之后,有时候用store接收返回值,有时候又用useStore接收返回值是怎么回事?

// 简化一下create
const create = (createState) => {
    // 这里的api就是上面createStore的返回值
    const api = typeof createState === 'function' ? createStore(createState) : createState
    // 定义useStore函数
    // 这里是基于react的hooks封装了 useStore 方法,用在react中
    // 其他场景直接使用api
    const useStore = (selector, equalityFn) => {}
    
    // 将api里的方法作为useStore的方法
    Object.assign(useStore, api)
    
    return useStore
}

2.2.2 看到这段就很明显,在create函数中,将createState的返回值作为了useStore的属性。这样在react的函数组件内把useStore作为hook使用;在函数外部或者非react环境,可以定义store = create(); { getState, setState, subscribe... } = store去使用zustand

2.3 useStore

2.3.1 问题3: useStore是怎么工作的?源码

const useStore = (selector = api.getState, equalityFn = Object.is) => {
    const [, forceUpdate] = useReducer(c => c + 1, 0);
    const state = api.getState();
    const stateRef = useRef(state);
    const selectorRef = useRef(selector);
    const equalityFnRef = useRef(equalityFn);
    const erroredRef = useRef(false);
    const currentSliceRef = useRef();

    if (currentSliceRef.current === undefined) {
      currentSliceRef.current = selector(state);
    }

    let newStateSlice;
    let hasNewStateSlice = false; 

    // 在监听器中已修改了stateRef.current = state 所以大部分情况是不会走进这个if语句里的
    if (stateRef.current !== state || selectorRef.current !== selector || equalityFnRef.current !== equalityFn || erroredRef.current) {
      // Using local variables to avoid mutations in the render phase.
      newStateSlice = selector(state);
      hasNewStateSlice = !equalityFn(currentSliceRef.current, newStateSlice);
    } // Syncing changes in useEffect.

    
    // 兼容SSR,有window就用useLayoutEffect,否则就用useEffect
    useIsoLayoutEffect(() => {
      if (hasNewStateSlice) {
        currentSliceRef.current = newStateSlice;
      }
        
      stateRef.current = state;
      selectorRef.current = selector;
      equalityFnRef.current = equalityFn;
      erroredRef.current = false;
    });
    
    const stateBeforeSubscriptionRef = useRef(state);
    useEffect(() => {
      const listener = () => {};

      const unsubscribe = api.subscribe(listener);
      
      return unsubscribe;
    }, []);
    return hasNewStateSlice ? newStateSlice : currentSliceRef.current;
  };

2.3.2 答案:这里其实就干了3件事情

    1. 缓存全局所有状态state,以及通过selector获取的局部状态
    1. useEffect中通过api中提供的subscribe注册监听器。这里就是典型的观察者模式。
try {
  // 获取下一个 所有状态,
  const nextState = api.getState();
  // 获取 局部状态
  const nextStateSlice = selectorRef.current(nextState);
  // 比较与上一次的状态是否有变化,如果有变化,强制更新
  if (!equalityFnRef.current(currentSliceRef.current, nextStateSlice)) {
    stateRef.current = nextState;
    currentSliceRef.current = nextStateSlice;
    forceUpdate();
  }
} catch (error) {
  erroredRef.current = true;
  forceUpdate();
}
    1. 强制更新会导致useStore重新执行,然后获取最新的局部变量
注册监听器 --> 更新状态  --> 比较局部状态是否变化 --是--> 更新状态
                              |
                              |
                              --否--> 不变化

2.4 middleware

2.4.1 问题4: zustand为什么可以像👇🏻 这样使用middleware呢?

// Log every time state is changed
const log = config => (set, get, api) => config(args => {
  console.log("  applying", args)
  set(args)
  console.log("  new state", get())
}, get, api)

// Turn the set method into an immer proxy
const immer = config => (set, get, api) => config((partial, replace) => {
  const nextState = typeof partial === 'function'
      ? produce(partial)
      : partial
  return set(nextState, replace)
}, get, api)

const useStore = create(
  log(
    immer((set) => ({
      bees: false,
      setBees: (input) => set((state) => void (state.bees = input)),
    }))
  ),
)

首先,根据上面的分析,可以知道

    1. create接受一个函数createState作为参数,且函数签名是这样的 (set, get, api) => { // ... },所以只要我们在调用create时,最终的输入是符合上述签名的函数就可以。
    1. createState的返回值是store中的数据来源state

所以我们可以利用高阶函数,在createState外层封装其他功能,只要保证签名和返回值与最初的结果一致就可以。

不考虑middleware情况:

const stateConfig = (set, get, api) => ({
  bees: false,
  setBees: (input) => set((state) => void (state.bees = input)),
})
const useStore = create(stateConfig)
// log调用 保证 签名一致
const log = (stateConfig) => {
    // 高阶函数内部调用stateConfig 保证 返回值一致
    return function(set, get, api) {          
        return stateConfig(args => {
            console.log('')
            set(args)
            console.log('')
        }, get, api)
    }
}

immer的场景,是在上面的基础之上,重写了setState

如果有业务需要,也可以对getter甚至api进行扩展

2.4.2 答案:只是高阶函数的使用

2.5 subscribe

2.5.1 subscribe是如何工作的?

2.5.2 回答: 这里subscribe就是实现了一个观察者模式了。set类型listerers负责收集监听器,在setState的时候回去触发所有的listeners

const subscribeWithSelector = (listener, selector = getState, equalityFn = Object.is) => {
    let currentSlice = selector(state);
    function listenerToAdd() {
      try {
        const newStateSlice = selector(state);
        // 比较当前slice的状态是否变化
        if (!equalityFn(currentSlice, newStateSlice)) {
          listener(currentSlice = newStateSlice);
        }
      } catch (error) {
        listener(null, error);
      }
    }

    listeners.add(listenerToAdd); // Unsubscribe
    // 取消,并删除监听器
    return () => listeners.delete(listenerToAdd);
  };

  const subscribe = (listener, selector, equalityFn) => {
    if (selector || equalityFn) {
      return subscribeWithSelector(listener, selector, equalityFn);
    }
    listeners.add(listener); // Unsubscribe
    return () => listeners.delete(listener);
  };

从源码可以看出来,在subscribe传入了selector或者equalityFn时,表示的是局部获取数据状态,这时如果内部状态变化,需要比较所依赖的数据是否有变化,通过equalityFn比对之后再去更新,这样就能做到局部更新

3. 总结

zustand主要使用观察者模式 + hook 实现的一个状态管理库,利用比较函数做到局部更新。