zustand实践与源码阅读

news2024/12/27 13:28:38

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

zustand是一个轻量、快速、可扩展的状态管理库。
目前在社区非常流行,现在github上有30K+的star。npm包的下载量,现在也仅次于redux,位于mobx之上,并且差距日益扩大。
在这里插入图片描述

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官网:https://awesomedevin.github.io/zustand-vue/docs/introduce/start/zustand
  • useSyncExternalStore:https://react.dev/reference/react/useSyncExternalStore
  • 学习 useSyncExternalStore:https://juejin.cn/post/7090063329913208868

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/994145.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

客户端SDK测试是什么?如何测?

01 是什么 客户端SDK是为第三方开发者提供的软件开发工具包&#xff0c;包括SDK接口、开发文档和Demo示例等。SDK和应用之间是什么关系呢&#xff1f;以云信即时消息服务为例&#xff0c;如下图所示&#xff0c;应用客户端通过调用云信SDK接口&#xff0c;进行消息等数据查询存…

华为云云耀云服务器L实例评测 | 零门槛入门使用教学

近年来&#xff0c;随着企业应用上云&#xff0c;云服务器一直备受用户的关注。特别是对于大多数的中小企业在上云的过程中&#xff0c;都希望能使用的是一种快速、简洁高效的云服务器。因为这样能尽可能地减轻企业运维的成本&#xff0c;同时又能方便企业的信息技术人员管理。…

Set和Map及哈希表介绍

搜索方式介绍TreeMapMap使用 TreeSetSet使用 Set和Map常用方法练习(后面补充)练习之Set/Mapoj练习&#xff08;后面补充&#xff09;哈希表哈希冲突避免冲突-哈希函数设计避免冲突-负载因子调节避免冲突-闭散列避免冲突-开散列 模拟实现哈希表哈希Map源码分析 搜索方式介绍 哈…

mysql的索引分类

索引分类 在 MySQL 数据库&#xff0c;将索引的具体类型主要分为以下几类&#xff1a;主键索引、唯一索引、常规索引、全文索引。 分类 含义 特点 关键字 主键 索引 针对于表中主键创建的索引 默认自动创建 , 只能 有一个 PRIMARY 唯一 索引 避免同一个表中某数据列中…

C语言“牵手”速卖通商品详情数据方法,速卖通商品详情API接口,速卖通API申请指南

速卖通是全球最大的自营式电商企业之一&#xff0c;在线销售计算机、手机及其它数码产品、家电、汽车配件、服装与鞋类、奢侈品、家居与家庭用品、化妆品与其它个人护理用品、食品与营养品、书籍与其它媒体产品、母婴用品与玩具、体育与健身器材以及虚拟商品等。 速卖通平台的…

nginx日志、nginx访问控制、nginx优化

总结 nginx监控 内存&#xff0c;网络&#xff0c;磁盘&#xff0c;cpu 对nginx监控可以监控什么&#xff1f; 1、监控nginx服务存活状况&#xff08;ss -antpl 、systemctl status nginx 、pa aux | grep nginx&#xff09; 2、对nginx的运行状态进行监控 3、 监控nginx的监…

进程的同步与互斥

相关概念 临界资源与临界区 临界资源&#xff1a;同一时刻只能由一个进程使用的资源。 如打印机、磁带机、绘图仪等物理设备&#xff1b;由不同进程共享的消息队列、变量、数据、文件等软件资源 临界区&#xff1a;程序中访问临界资源的那一部分代码 进入区、退出区、剩余区&a…

六、MySql表的增删改查

CRUD : Create(创建), Retrieve(读取)&#xff0c;Update(更新)&#xff0c;Delete&#xff08;删除&#xff09; 文章目录 一、Create&#xff08;一&#xff09;语法&#xff08;二&#xff09;案例&#xff08;三&#xff09;插入情况1.单行数据 全列插入2.多行数据 指定…

Matlab之数组字符串函数汇总

一、前言 在MATLAB中&#xff0c;数组字符串是指由字符组成的一维数组。字符串可以包含字母、数字、标点符号和空格等字符。MATLAB提供了一些函数和操作符来创建、访问和操作字符串数组。 二、字符串数组具体怎么使用&#xff1f; 1、使用单引号或双引号括起来的字符序列 例…

日常开发小汇总(5)数组克隆、伪数组转换为真数组、随机排序

slice 切割数组实现克隆是浅拷贝 let arr [1,2, {name:1}] let newarr arr.slice(0); console.log(newarr) //[1,2, {name:1}] newarr[2].name 666; console.log(arr[2].name);//666 JSON实现克隆 深拷贝 let arr [1,2, {name:1}] let newarr JSON.parse( JSON.stringi…

【JS面试题】如何通过闭包漏洞在外部修改函数中的变量

✍️ 作者简介: 前端新手学习中。 &#x1f482; 作者主页: 作者主页查看更多前端教学 &#x1f393; 专栏分享&#xff1a;css重难点教学 Node.js教学 从头开始学习 ajax学习 前端面试题 文章目录 什么是闭包例 如何在函数外部修改闭包中变量 什么是闭包 闭包这个东西对新…

linux--进程通信--管道通信

IPC是各种进程间通信方式的统称。 进程间通信&#xff1a;是指在不同进程之间传播或交换信息。 IPC的方式通常有&#xff1a; 单机&#xff1a;管道&#xff08;包括无名管道和命名管道&#xff09;、消息队列、信号量、共享存储、 多机&#xff1a;Socket、Streams等 1、管道…

【计算机组成原理】十个问题带你走进计算机组成的世界

十个问题带你走进计算机组成的世界 你知道 a 1 2 这条代码是怎么被 CPU 执行的吗&#xff1f; 在计算机中&#xff0c;数据和指令是分开区域存放的&#xff0c;存放指令的区域的地方称为正文段&#xff0c;存放数据的区域称为数据段。 例如下图中&#xff0c;数据1和数据2…

Redis配置

关系型数据库和非关系型数据库 ①了解关系和非关系 关系型数据库 一个结构化的数据库&#xff0c;创建在关系模型基础上&#xff0c;一般面向于记录&#xff0c;包括Oracle、MySQL、SQL Server、Microsoft Access、DB2、postgreSQL等 非关系型数据库 除了主流的关系型数据库…

java 歌词解析 源代码, 在windows10下调试运行成功。

需要两个素材。 歌词与音乐.wav package week3.exam6;public class Info {private final String info;public Info(String info){this.infoinfo;}public String getInfo() {return info;}public String toString(){return info;} }package week3.exam6;public class Lyric ext…

华为云新用户云服务器优惠价格表

华为云服务器作为业界领先的云服务提供商之一&#xff0c;一直致力于为全球用户提供高效、稳定、安全的云服务。为了帮助新用户更好地了解华为云服务器的价格和优惠活动&#xff0c;本文将详细介绍华为云服务器对新用户的优惠价格表。 一、华为云耀云服务器L实例价格表 华为云…

JavaScript基础知识09——数据类型

哈喽&#xff0c;大家好啊&#xff0c;这里是雷工笔记&#xff0c;我是雷工。 数据类型比较常见&#xff0c;无论是对程序员&#xff0c;还是电气工程师来说&#xff0c;都再熟悉不过了&#xff0c;这里跟着教程了解一下&#xff0c;主要看跟自己以往在其他PLC&#xff0c;C#&a…

2023-09-09 LeetCode每日一题(课程表)

2023-09-09每日一题 一、题目编号 207. 课程表二、题目链接 点击跳转到题目位置 三、题目描述 你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出&#xff0c;其中…

【牛客面试必刷TOP101】Day2.判断链表中是否有环和链表中倒数最后k个结点

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训选择题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01;&#xff…

python-jieba库

jieba库&#xff0c;python提供的中文分词函数库的第三方库&#xff0c;它可以将一段中文文本分割成中文词语序列。 安装jieba库 pip install jiebajieba的三个模式 全模式 - - - jieba.lcut(s,cut_allTrue) - - - 速度非常快&#xff0c;但有冗余数据 精确模式&#xff08;…