Zustand 源码阅读计划(3)- JS 篇 - Middlewares 中间件逻辑

更好的阅读体验:Zustand 源码阅读计划(3)- JS 篇 - Middlewares 中间件逻辑 - 薄物细故集

本文来学习 zustand 的中间件机制

核心目标:设计一个通用、易于适配,可以多个中间件任意组合嵌套的中间件系统

设计思路

作为一个状态管理库,最重要的也就是 获取的行为、设置的行为、状态值 这三个东西。只要能控制住这些,就能自由实现各种功能。

而对于 zustand 来说,获取值是直接从对象中解构的,对获取的行为无法拦截,状态值在中间件阶段拿不到。

而鉴于 zustand 中,创建 store 就会有一次初始值的 set,且之后任何对值的修改都是需要执行 set 的,所以 设置的行为 是一个非常好的选择。

并且选择 set 还有一个更好的优势,内部存储的数据和获取到的是一致的。减少了不同地方获取到的值有差异的问题,更好维护。

代码分析

中间件的本质就是一个 高阶函数

js 复制代码
const myMiddleware = (createState, options = {}) => (set, get, api) => {
  // ... 中间件的逻辑 ...
  const newSet = (partial, replace) => {
	// 拿到最新的状态值
	let nextState = typeof partial === "function" ? partial(get()) : partial;
	// 调用更上层的 set 方法
	set(nextState, replace)
  }
  
  // 调用原始的 createState,可能传入一个被修改过的 set 函数
  return createState(newSet, get, api); 
};

createState 就是最核心的 store 定义,例如:

js 复制代码
const createState = (set) => ({
  count: 0,
  increment: () => set((state) => {
    console.log('increment', state.count)
    return { count: state.count + 1 }
  }),
})

我们就是通过对 set 更改,传入 createState 中,从而实现的对整个 store 做自定义的逻辑。

示例

某个需求是将 store 内的数字全部转化为保留两位小数有什么办法?

最容易想到的是每个值在设置的时候都调用一下 toFixed 方法不就行了?但如果需求变更,要保留三位或者要转化为字符串,那这个修改就会比较多了。

当这个需求统一的时候,我们就可以使用中间件的形式来对数据进行处理。

js 复制代码
export function toString(createState, options = {}) {
  return (set, get, api) => {

    // 为什么这个 newSet 就能生效?
    // 因为回调一层一层的,先最底层的回调,然后一层一层的回调,最后到最外层的回调
    // 也就是说,实际写的值和方法先给 vallina 处理后,逐层往上
    // 所以这里的 newSet 添加额外的逻辑,用更底层的 set 去赋值
    // 将 newSet 在更外层,以及最终返回值的 setState 中都能生效
    //
    // 一个更直观的例子,如果取 get(),值是空的
    // 就是因为在这个执行阶段,最内层的定义还没执行到
    const newSet = (partial, replace) => {
      console.log("newSet", partial);
      // 拿到最新的值
      let nextState = typeof partial === "function" ? partial(get()) : partial;
      const keys = Object.keys(nextState)
      keys.forEach(key => {
        if (typeof nextState[key] === 'number') {
          nextState[key] = nextState[key].toFixed(2)
        }
      })
      set(nextState, replace);
    };

    return createState(newSet, get, api);
  };
}

解析:

zustand 中间件是洋葱模型,逐层嵌套,以

js 复制代码
const useStore = create()(A(B((set) => ({
  count: 0,
  increment: () => set((state) => {
    console.log('increment', state.count)
    return { count: state.count + 1 }
  }),
}))))

// 中间件

const A = (createState, options = {}) => (set, get, api) => {
  // ... 中间件的逻辑 ...
  const newSet = (partial, replace) => {
	console.log('A-1')
	// 拿到最新的状态值
	let nextState = typeof partial === "function" ? partial(get()) : partial;
	// 调用更上层的 set 方法
	set(nextState, replace)
	console.log('A-2')
  }
  
  // 调用原始的 createState,可能传入一个被修改过的 set 函数
  return createState(newSet, get, api); 
};

const B = (createState, options = {}) => (set, get, api) => {
  // ... 中间件的逻辑 ...
  const newSet = (partial, replace) => {
	console.log('B-1')
	// 拿到最新的状态值
	let nextState = typeof partial === "function" ? partial(get()) : partial;
	// 调用更上层的 set 方法
	set(nextState, replace)
	console.log('B-2')
  }
  
  // 调用原始的 createState,可能传入一个被修改过的 set 函数
  return createState(newSet, get, api); 
};

举例:

createState 传给 B,B 中的 newSet 对传入的 set 包装一下,做了自己额外逻辑,再将 newSet 传递给 createState 并返回

这样 A 接收到的 createState 实际是 B 返回的 createState(newSet, get, api),A 中处理了 newSet 后,也调用 B 传入的 createState,将自己的 newSet 传入其中

A 返回的 createState(newSet, get, api) 则是被 create 处理,传入了 Vanilla 的 createStore,将 api 中的 setState 等内容传入 createState 调用。

  1. setState_set -> A_createState
  2. A_set(setState_set) -> B_createState
  3. B_set(A_set) -> C_createState
  4. C_set(B_set) -> config_createState

这样调用链路就清晰了。

config_createState 内执行了 set 的时候,就是从 C_set 开始执行,往上追溯到 B_set、A_set、setState_set。

结论:从最内层的中间件开始逐层往外执行。

所以,当调用 set 的时候,输出顺序如下:

  1. C-1
  2. B-1
  3. A-1
  4. Vanilla
  5. A-2
  6. B-2
  7. C-2

结语

zustand 的中间件从功能上还是很简单的,通过核心参数的逐层传递,就能很便捷支持自定义的中间件

相关推荐
儒雅的烤地瓜6 分钟前
JS | 如何把一个伪数组转换成一个真正的数组?
javascript·from方法·数组转换·扩展运算符·slice方法·push方法
my一阁30 分钟前
一文解决Chrome使用
前端·chrome
IT_陈寒34 分钟前
SpringBoot性能调优实战:5个让接口响应速度提升300%的关键配置
前端·人工智能·后端
訾博ZiBo1 小时前
告别 v-model 焦虑:在 React 中优雅地处理『双向绑定』
前端·react.js
β添砖java2 小时前
交互动效设计
前端·javascript·交互
简小瑞2 小时前
VSCode用它管理上千个服务:依赖注入从入门到实战
前端·设计模式
用户56170657111472 小时前
scratch二次开发--如何在舞台区开启网络摄像头(Turbowarp版)
javascript
骑自行车的码农2 小时前
React 合成事件的设计原理 2
前端·react.js
JamesGosling6662 小时前
详解 Vue 3.6 Vapor Mode:从原理到问题,看透 VDOM 逐步退场的底层逻辑
前端·vue.js
一个很帅的帅哥3 小时前
Vue中的hash模式和history模式
前端·vue.js·history模式·hash模式