首页 > 其他分享 >手写一个 Zustand,只要 60 行

手写一个 Zustand,只要 60 行

时间:2023-12-24 18:37:54浏览次数:20  
标签:Zustand const zustand selector 60 state api return 手写

提到状态管理,大家可能首先想到的是 redux。

redux 是老牌状态管理库,能完成各种基本功能,并且有着庞大的中间件生态来扩展额外功能。

但 redux 经常被人诟病它的使用繁琐。

近两年,React 社区出现了很多新的状态管理库,比如 zustand、jotai、recoil 等,都完全能替代 redux,而且更简单。

zustand 算是其中最流行的一个。

看 star 数,redux 有 60k,而 zustand 也有 38k 了:

redux:

手写一个 Zustand,只要 60 行_中间件

zustand:

手写一个 Zustand,只要 60 行_javascript_02

看 npm 包的周下载量,redux 有 880w,而 zustand 也有 260w 了:

redux:

手写一个 Zustand,只要 60 行_React.js_03

zustand:

手写一个 Zustand,只要 60 行_React.js_04

从各方面来说,zustand 都在快速赶超 redux。

那 zustand 为什么会火起来呢?

我觉得主要是因为简单,zustand 用起来真的是没有什么学习成本,没有 redux 的 action、reducer 等概念。

而且功能很强大,zustand 也有中间件,可以给它额外扩展功能。

既然功能上能替代 redux,那为什么不选择一个更简单的呢?

下面我们就来试试看:

npx create-react-app zustand-test

手写一个 Zustand,只要 60 行_React.js_05

用 cra 创建个 react 项目。

进入项目把它跑起来:

npm run start

浏览器访问下:

手写一个 Zustand,只要 60 行_JavaScript_06

没啥问题。

然后安装 zustand:

npm install --save zustand

改下 App.js

import { create } from 'zustand'

const useXxxStore = create((set) => ({
  aaa: '',
  bbb: '',
  updateAaa: (value) => set(() => ({ aaa: value })),
  updateBbb: (value) => set(() => ({ bbb: value })),
}))

export default function App() {
  const updateAaa = useXxxStore((state) => state.updateAaa)
  const aaa = useXxxStore((state) => state.aaa)

  return (
    <div>
        <input
          onChange={(e) => updateAaa(e.currentTarget.value)}
          value={aaa}
        />
        <Bbb></Bbb>
    </div>
  )
}

function Bbb() {
  return <div>
    <Ccc></Ccc>
  </div>
}

function Ccc() {
  const aaa = useXxxStore((state) => state.aaa)
  return <p>hello, {aaa}</p>
}

用 create 函数创建一个 store,定义 state 和修改 state 的方法。

手写一个 Zustand,只要 60 行_javascript_07

然后在组件里调用 create 返回的函数,取出属性或者方法在组件里用:

手写一个 Zustand,只要 60 行_中间件_08

手写一个 Zustand,只要 60 行_前端_09

这就是 zustand 的全部用法了,就这么简单。

手写一个 Zustand,只要 60 行_中间件_10

有的同学说,不是还有中间件么?

其实中间件并不是 zustand 自己实现的功能。

你看这个 create 方法的参数,它是一个接受 set、get、store 的三个参数的函数:

手写一个 Zustand,只要 60 行_JavaScript_11

那我们可不可以包一层,自己拿到 get、set、store,对这些做一些修改,之后返回一个接受三个参数的函数呢?

比如这样:

function logMiddleware(func) {
  return function(set, get, store) {

    function newSet(...args) {
      console.log('调用了 set,新的 state:', get());
      return set(...args)
    }
  
    return func(newSet, get, store)
  }
}

我接受之前的函数,然后对把 set、get、store 修改之后再调用它:

手写一个 Zustand,只要 60 行_React.js_12

这样不就给 zustand 的 set 方法加上了额外的功能么?

手写一个 Zustand,只要 60 行_前端_13

这个就是中间件,和 redux 的中间件是一样的设计。

它并不需要 zustand 本身做啥支持,只要把 create 的参数设计成一个函数,这个函数接收 set、get 等函作为参数,那就自然支持了中间件。

zustand 内置了一些中间件,比如 immer、persist。

persist 就是同步 store 数据到 localStorage 的。

我们试一下:

手写一个 Zustand,只要 60 行_中间件_14

效果如下:

手写一个 Zustand,只要 60 行_前端_15

而且,中间件是可以层层嵌套的:

我们把自己写的 log 和内置的 persist 结合起来:

手写一个 Zustand,只要 60 行_JavaScript_16

效果如下:

手写一个 Zustand,只要 60 行_中间件_17

手写一个 Zustand,只要 60 行_React.js_18

因为中间件不就是修改 set、get 这些参数么,这些 set、get 是可以层层包装的,所以自然中间件也就可以层层嵌套。

redux 和 zustand 的中间件一脉相承,都是很巧妙的设计。

学完了 zustand 的功能后,你觉得写这样一个 zustand 需要多少代码呢?

其实不到 100 行就能搞定。

不信我们试试看:

const createStore = (createState) => {
    let state;
    const listeners = new Set();
  
    const setState = (partial, replace) => {
      const nextState = typeof partial === 'function' ? partial(state) : partial

      if (!Object.is(nextState, state)) {
        const previousState = state;

        if(!replace) {
            state = (typeof nextState !== 'object' || nextState === null)
                ? nextState
                : Object.assign({}, state, nextState);
        } else {
            state = nextState;
        }
        listeners.forEach((listener) => listener(state, previousState));
      }
    }
  
    const getState = () => state;
  
    const subscribe= (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    }
  
    const destroy= () => {
      listeners.clear()
    }
  
    const api = { setState, getState, subscribe, destroy }

    state = createState(setState, getState, api)

    return api
}

state 是全局状态,listeners 是监听器。

然后 setState 修改状态、getState 读取状态、subscribe 添加监听器、destroy 清除所有监听器。

这些都很容易理解。

至于 replace,这是 zustand 在 set 状态的时候默认是合并,你也可以传一个 true 改成替换:

手写一个 Zustand,只要 60 行_JavaScript_19

那如果状态变了,如何触发渲染呢?

useState 就可以。

这样写:

function useStore(api, selector) {
    const [,forceRender ] = useState(0);
    useEffect(() => {
        api.subscribe((state, prevState) => {
            const newObj = selector(state);
            const oldobj = selector(prevState);

            if(newObj !== oldobj) {
                forceRender(Math.random());
            }       
        })
    }, []);
    return selector(api.getState());
}

selector 说的是传入的这个函数:

手写一个 Zustand,只要 60 行_React.js_20

我们用 useState 设置随机数来触发渲染。

监听 state 的变化,变了之后,根据新旧 state 调用 selector 函数的结果,来判断是否需要重新渲染。

然后定义 create 方法:

export const create = (createState) => {
    const api = createStore(createState)

    const useBoundStore = (selector) => useStore(api, selector)

    Object.assign(useBoundStore, api);

    return useBoundStore
}

它就是先调用 createStore 创建 store。

然后返回 useStore 的函数,用于组件内调用。

测试下:

把 create 函数换成我们自己的,其余代码不变:

手写一个 Zustand,只要 60 行_中间件_21

可以看到,功能依然正常:

手写一个 Zustand,只要 60 行_javascript_22

手写一个 Zustand,只要 60 行_前端_23

我们的 my-zustand 已经能够完美替代 zustand 了。

全部代码如下:

import { useEffect, useState } from "react";

const createStore = (createState) => {
    let state;
    const listeners = new Set();
  
    const setState = (partial, replace) => {
      const nextState = typeof partial === 'function' ? partial(state) : partial

      if (!Object.is(nextState, state)) {
        const previousState = state;

        if(!replace) {
            state = (typeof nextState !== 'object' || nextState === null)
                ? nextState
                : Object.assign({}, state, nextState);
        } else {
            state = nextState;
        }
        listeners.forEach((listener) => listener(state, previousState));
      }
    }
  
    const getState = () => state;
  
    const subscribe= (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    }
  
    const destroy= () => {
      listeners.clear()
    }
  
    const api = { setState, getState, subscribe, destroy }

    state = createState(setState, getState, api)

    return api
}

function useStore(api, selector) {
    const [,forceRender ] = useState(0);
    useEffect(() => {
        api.subscribe((state, prevState) => {
            const newObj = selector(state);
            const oldobj = selector(prevState);

            if(newObj !== oldobj) {
                forceRender(Math.random());
            }       
        })
    }, []);
    return selector(api.getState());
}

export const create = (createState) => {
    const api = createStore(createState)

    const useBoundStore = (selector) => useStore(api, selector)

    Object.assign(useBoundStore, api);

    return useBoundStore
}

60 多行代码。

其实,代码还可以进一步简化。

react 有一个 hook 就是用来定义外部 store 的,store 变化以后会触发 rerender:

手写一个 Zustand,只要 60 行_React.js_24

有了这个 useSyncExternalStore 的 hook,我们就不用自己监听 store 变化触发 rerender 了:

手写一个 Zustand,只要 60 行_中间件_25

可以简化成这样:

function useStore(api, selector) {
    function getState() {
        return selector(api.getState());
    }
    
    return useSyncExternalStore(api.subscribe, getState)
}

这样,my-zustand 就完美了。

import { useSyncExternalStore } from "react";

const createStore = (createState) => {
    let state;
    const listeners = new Set();
  
    const setState = (partial, replace) => {
      const nextState = typeof partial === 'function' ? partial(state) : partial

      if (!Object.is(nextState, state)) {
        const previousState = state;

        if(!replace) {
            state = (typeof nextState !== 'object' || nextState === null)
                ? nextState
                : Object.assign({}, state, nextState);
        } else {
            state = nextState;
        }
        listeners.forEach((listener) => listener(state, previousState));
      }
    }
  
    const getState = () => state;
  
    const subscribe= (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    }
  
    const destroy= () => {
      listeners.clear()
    }
  
    const api = { setState, getState, subscribe, destroy }

    state = createState(setState, getState, api)

    return api
}

function useStore(api, selector) {
    function getState() {
        return selector(api.getState());
    }
    
    return useSyncExternalStore(api.subscribe, getState)
}

export const create = (createState) => {
    const api = createStore(createState)

    const useBoundStore = (selector) => useStore(api, selector)

    Object.assign(useBoundStore, api);

    return useBoundStore
}

有的同学可能会质疑,zustand 的源码就这么点么?

我们调试下就知道了:

点击 vscode 的 create a launch.json file,创建一个调试配置:

手写一个 Zustand,只要 60 行_JavaScript_26

改下调试的端口,点击调试启动:

手写一个 Zustand,只要 60 行_中间件_27

把 zustand 换成之前的,然后打个断点:

手写一个 Zustand,只要 60 行_中间件_28

通过调试,可以看到 create 的实现如下:

手写一个 Zustand,只要 60 行_JavaScript_29

而 useStore 的实现如下:

手写一个 Zustand,只要 60 行_JavaScript_30

唯一的区别就是它用的是一个 shim 包里的,因为它要保证这个 hook 的兼容性。

所以说,我们通过 60 行代码实现的,就是一比一复刻的 zustand。

至此,zustand 还有一个非常大的优点就呼之欲出了:体积小。

一共也没多少代码,压缩后能多大呢?只有 1kb。

总结

近几年出了很多可以替代 redux 的优秀状态管理库,zustand 是其中最优秀的一个。

它的特点有很多:体积小、简单、支持中间件扩展。

它的核心就是一个 create 函数,传入 state 来创建 store。

create 返回的函数可以传入 selector,取出部分 state 在组件里用。

它的中间件和 redux 一样,就是一个高阶函数,可以对 get、set 做一些扩展。

zustand 内置了 immer、persist 等中间件,我们也自己写了一个 log 的中间件。

zustand 本身的实现也很简单,就是 getState、setState、subscribe 这些功能,然后再加上 useSyncExternalStore 来触发组件 rerender。

一共也就 60 行代码。

这样一个简单强大、非常流行的状态管理库,你确定不自己手写一个试试么?

标签:Zustand,const,zustand,selector,60,state,api,return,手写
From: https://blog.51cto.com/u_15506823/8956365

相关文章

  • 在统信UOS操作系统1060上如何部署DNS服务器?01
    原文链接:在统信UOS操作系统1060上如何部署DNS服务器?01hello,大家好啊!今天我要给大家带来的是在统信UOS操作系统1060上部署DNS服务器系列的第一篇文章。在这个系列中,我们将一步步搭建一个完整的DNS服务器环境。而今天,我们的第一步是搭建一个测试用的HTTP服务器。这个过程相对简单,但它......
  • 安装统信UOS服务器操作系统1060
    原文链接:安装统信UOS服务器操作系统1060hello,大家好啊!今天我要给大家介绍的是如何安装统信UOS服务器操作系统1060。统信UOS是一款基于Linux内核,专为中国市场定制开发的操作系统。它不仅提供了良好的用户体验,还在安全性和稳定性方面进行了大量优化。对于那些寻求替代传统服务器操作......
  • 360沃通亮相2023年深圳市卫生健康信息学术会议,展示医疗行业商密应用方案
    2023年12月15日-16日,深圳市卫生健康信息协会举办主题为“智慧健康引领网络安全护航”的2023年深圳市卫生健康信息学术会议暨“京沪宁深连线”深圳专场,360沃通作为深圳密码领域代表性企业受邀参会,与现场知名专家学者、卫生健康信息化业内同仁、卫生健康信息产品厂商展开深入交流,并......
  • MT6785/MT6359/MT6360/MT6186/MT6631 UFS_LPDDR4X原理图
    联发科MT6785核心板是一款高度集成的基带平台,集成了蓝牙、FM、WLAN和GPS模块,旨在支持LTE/LTE-A和C2K智能手机应用。这款芯片采用了两个最高频率可达到2.05GHz的ARM®Cortex-A76核心和六个最高频率可达到2.0GHz的ARM®Cortex-A55核心,搭载ArmMali-G76MC4GPU运行速度可提升至......
  • Educational Codeforces Round 160 (Rated for Div. 2)
    A.RatingIncrease字符串处理#include<bits/stdc++.h>usingnamespacestd;voidsolve(){ strings; cin>>s; intn=s.size(); s=""+s; for(inti=1;i<=n-1;i++){ stringt=""; for(intj=1;j<=i;j++){ t=t+s[j]; } ......
  • 560. 和为 K 的子数组
    给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。子数组是数组中元素的连续非空序列。 示例1:输入:nums=[1,1,1],k=2输出:2示例2:输入:nums=[1,2,3],k=3输出:2 提示:1<=nums.length<=2*104-1000<=nu......
  • CF Edu160D Array Collapse
    可以操作任意多次。考虑dp。设\(dp_i\)表示考虑前\(i\)个位置之后,强制最终留下第\(i\)个位置上的数的方案数,转移时枚举前面的位置\(j\),对于合法的决策\(j\),显然需满足\(\forallk\in(j,i)\),\(a_k>a_i\)或\(a_k>a_j\)。显然可以提前预处理出每个位置\(i\)向前第一......
  • CF Edu160E Matrix Problem
    场上疯狂想求任意解+改动解至最优。。想不下去的时候一定要再读一遍题跳出来啊。限制每一行每一列的\(1\)的个数,这很匹配啊!!考虑网络流,左侧\(n\)个节点连流量\(a_i\),右侧\(m\)个节点连流量\(b_i\)。对于原矩阵中为\(0\)的项\((i,j)\),若新矩阵中为\(0\)则流量为\(0......
  • CF Edu160F Palindromic Problem
    赛时过的人少估计是因为难调。考虑修改一个字符的贡献,会使得所有以该字符为瓶颈的回文串增加长度,同时会使得原来所有最长回文串经过该位置的位置减少长度。换个视角,不妨通过二分+哈希分别预处理出以每个位置为回文中心的最长回文串长度、以及修改一个字符后的最长回文串长度,则对......
  • GPT-4没通过图灵测试!60年前老AI击败了ChatGPT,但人类胜率也仅有63%
    长久以来,「图灵测试」成为了判断计算机是否具有「智能」的核心命题。上世纪60年代,曾由麻省理工团队开发了史上第一个基于规则的聊天机器人ELIZA,在这场测试中失败了。时间快进到现在,「地表最强」ChatGPT不仅能作图、写代码,还能胜任多种复杂任务,无「LLM」能敌。然而,ChatGPT却在最近一......