zustand实践与源码阅读

如何管理数据?
日常使用:发布订阅、context、redux…

zustand是一个轻量、快速、可扩展的状态管理库。
目前在社区非常流行,现在github上有30K+的star。npm包的下载量,现在也仅次于redux,位于mobx之上,并且差距日益扩大。
zustand实践与源码阅读_第1张图片

zustand 德语 “状态”、jotai 日语 “状态”、valtio 芬兰语
“状态”,这三个都是状态管理库,作者是同一个人:Daishi Kato。

1 zustand over redux?

  1. 学习成本低:全面拥抱hooks,仅有1个核心api,无其它库依赖。
    redux:学习曲线陡峭,大多数情况下需要配合其它库和中间件才能工作。

  2. 开发者体验好:无需Provider,无啰嗦的模板代码,异步处理就是普通的async/await。
    redux:模板代码饱受诟病,action、actionType、reducer… 异步处理依赖中间,写法相对麻烦。

  3. 体积更轻量:1.1KB gzipped。
    redux:redux(1.8KB) + react-redux(4.7KB) + redux-saga(5KB) = 11.5KB。
    mobx:16.5KB。
    recoil:23.5KB。
    jotai:2.4KB。
    valtio:3KB

2 如何使用

2.1创建一个store

import { create } from 'zustand';

interface CountState {
  // 数值
  count: number;
  // 增加
  increment: () => void;
  // 减少
  decrement: () => void;
}

// 创建初始state
const createInitStateFn = (set) => ({
  count: 0, // 初始值
  increment() {
    set((state) => ({
      count: state.count + 1,
    }));
  },
  decrement() {
    set((state) => ({
      count: state.count - 1,
    }));
  },
});

// 创建状态存储
const useCountStore = create<CountState>(createInitStateFn);

2.2绑定组件

const Counter = () => {
 const { count, increment, decrement } = useCountStore();

 return (
   <div>
     <p>{count}</p>
     <button onClick={increment}>+</button>
     <button onClick={decrement}>-</button>
   </div>
 );
};

export default Counter;

2.3 状态派生

count=0 绿色
count<0 红色
count>0 蓝色

// selector
const colorSelector = (state) => {
  if (state.count === 0) {
    return 'green';
  }
  
  if (state.count < 0) {
    return 'red';
  }

  return 'blue';
};

// Counter
const Counter = () => {
  const { count, increment, decrement } = useCountStore();
  const color = useCountStore(colorSelector);

  return (
    <div>
      <p style={{ color }}>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

export default Counter;

2.4 中间件拓展

// 添加日志打印功能
const log = (config) => (set, get, api) =>
  config(
    (...args) => {
      console.log('  applying', args)
      set(...args)
      console.log('  new state', get())
    },
    get,
    api
  )
const useCountStore = create(log(createInitStateFn))

2.5 实例:主接口数据管理

/*
 * @Name: 全局数据管理
 * @Date: 2023-08-13 10:00:41
 * @author: xiaohongru.xhr
 */
import { create } from 'zustand';
import Service from '@/service';
import { query } from '@ali/tl-detector';
import { IHome } from '@/types';

const initialState: IHome = {} as IHome;

interface IGlobalStore {
  isLoading: boolean;
  isError: boolean;
  homeData: IHome;
  fetchHomeData: () => Promise<any>;
  receiveAward: (type: string) => Promise<any>;
}

export const useGlobalStore = create<IGlobalStore>()((set, get) => ({
  isLoading: true,
  isError: false,
  homeData: initialState,
	// 查询主接口
  fetchHomeData: () => {
    return Service.queryHomeData({
      fromSource: query?.fromSource,
    })
      .then((res) => {
        set({ isLoading: false, isError: false, homeData: res?.result });
        return Promise.resolve(res?.result);
      })
      .catch((err) => {
        set({ isLoading: false, isError: true });
        return Promise.resolve(err);
      });
  },

  // 领取奖品接口
  receiveAward: (type) => {
    return Service.queryAward({
      type,
    })
      .then((res) => {
        // 领取奖励后刷主接口
        get().fetchHomeData(); 
        return Promise.resolve(res);
      })
      .catch((err) => {
        return Promise.resolve(err);
      });
  },
}));

// 主接口状态 
export const useIsError = () => useGlobalStore((state) => state.isError);
export const useIsLoading = () => useGlobalStore((state) => state.isLoading);

// 一些操作后需要重新刷主接口
export const useFetchHomeData = () => useGlobalStore((state) => state.fetchHomeData);

// 主接口数据
export const useHomeData = () => useGlobalStore((state) => state.homeData);

3 实现

3.1 一句话

zustand = 发布订阅 + react hooks

核心:create函数

import { create } from 'zustand';

const useCountStore = create(fn);

create传入参数是一个函数fn,返回值也是一个函数useCountStore

// 问题:如何处理入参数fn, 如何得到返回值
const create = (fn) => {
  // 创建store,返回操作store的api
  const api = isFunction(fn)? createStore(fn) : fn;

  // 通过hooks 挂载到页面上
  const useBoundStore = (seletctor, equalityFn) => {
    return useStore(api, selector, equalityFn);
  }

  Object.assign(useBoundStore, api);
  return useBoundStore;
}

3.3 createStore 发布订阅

  1. 构造一个store的结构
  2. 用fn创建一个store实例
const createStore = (fn) => {
  let state;
  const listeners: Set<Listener> = new Set();

  // get
  const getState = () => state;

  // set
  const setState = (partial: any, replace: boolean) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial;

    if (!Object.is(nextState, state)) {
      const prev = state;
      state = replace ? nextState : Object.assign({}, state, nextState);
      listeners.forEach((listener) => listener(state, prev));
    }
  };

  // 添加订阅
  const subscribe = (listener) => {
    listeners.add(listener);

    // Unsubscribe
    return () => {
      listeners.delete(listener);
    };
  };

  // 清理订阅
  const destroy = () => {
    listeners.clear();
  };

  const api = { setState, getState, subscribe, destroy };

  // 给state赋值
  state = fn(setState, getState, api);

  return api;
};

3.4 挂载状态

如何实现store中的值变成页面的state

// 绑定hooks
const useStore = (api, selector, equalityFn) =>{
  const [state, setState] = useState(api.getState());

  useEffect(()=>{
    // state对象中的一个key的value变化,会改变整个对象地址,需要selector,进行优化
    const unsubscribe = api.subscribe(state => setState(selector(state)));
    
    return unbscribe;
  },[])

  return state;
}

使用react 新hooks:useSyncExternalStore实现

export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void, 
  getSnapshot: () => T, 
  getServerSnapshot?: () => T,
): T
  • subscribe:注册回调的函数,返回一个() => void 用于清除副作用函数,每当 store 更改时调用该回调函数触发组件更新
  • getSnapshot:返回对应(想要)的 store
  • getServerSnapshot:返回服务器渲染期间使用的快照的函数,一般般用于 SSR 场景
// 用useSyncExternalStore实现useStore
const useStore=(api, selector, equalityFn)=>{
	const value = useSyncExternalStore(api.subscribe, ()=> selector(api.getState()));
  return value;
}

完整实现

import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';

const useStore = (api, selector, equalityFn) => {
  
  // 针对equalityFn来对更新前后数据进行对比
  const instRef = useRef(null);
  let inst;

  if (instRef.current === null) {
    inst = {
      hasValue: false,
      value: null,
    };
    instRef.current = inst;
  } else {
    inst = instRef.current;
  }

  const _useMemo = useMemo(
    function () {
      let hasMemo = false;
      let memoizedSnapshot;
      let memoizedSelection;

      const memoizedSelector = function (nextSnapshot) {
        if (!hasMemo) {
          // 第一次调用钩子时,没有记忆结果
          hasMemo = true;
          memoizedSnapshot = nextSnapshot;

          var _nextSelection = selector(nextSnapshot);

          if (equalityFn !== undefined) {
            // 即使选择器已更改,当前呈现的选择也可能等于新选择。 如果可能的话,我们应该尝试重用当前值,以保留下游记忆
            if (inst.hasValue) {
              var currentSelection = inst.value;

              if (equalityFn(currentSelection, _nextSelection)) {
                memoizedSelection = currentSelection;
                return currentSelection;
              }
            }
          }

          memoizedSelection = _nextSelection;
          return _nextSelection;
        } 

        const prevSnapshot = memoizedSnapshot;
        const prevSelection = memoizedSelection;

        if (Object.is(prevSnapshot, nextSnapshot)) {
          // 快照与上次相同。 重复使用之前的选择
          return prevSelection;
        } 

        // 快照已更改,因此我们需要计算新的选择
        const nextSelection = selector(nextSnapshot); 

        // 如果提供了自定义 equalFn 函数,请使用它来检查数据是否已更改。 
        // 如果没有,则返回之前的选择。 
        // 这向 React 发出信号,表明选择在概念上是相等的,我们可以摆脱渲染
        if (
          equalityFn !== undefined &&
          equalityFn(prevSelection, nextSelection)
        ) {
          return prevSelection;
        }

        memoizedSnapshot = nextSnapshot;
        memoizedSelection = nextSelection;
        return nextSelection;
      }; 

      const getSnapshotWithSelector = function () {
        return memoizedSelector(api.getState());
      };

      return [getSnapshotWithSelector];
    },
    [api.getState, selector, equalityFn]
  );
  const getSelection = _useMemo[0];

  // 挂在到state上
  let value = useSyncExternalStore(api.subscribe, getSelection);

  useEffect(() => {
    inst.hasValue = true;
    inst.value = value;
  }, [value]);

  return value;
};

4 总结

从页面视角更新过程:
zustand实践与源码阅读_第2张图片

参考资料

  • zustand官网:https://awesomedevin.github.io/zustand-vue/docs/introduce/start/zustand
  • useSyncExternalStore:https://react.dev/reference/react/useSyncExternalStore
  • 学习 useSyncExternalStore:https://juejin.cn/post/7090063329913208868

你可能感兴趣的:(javascript,开发语言,前端,react.js)