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 的中间件从功能上还是很简单的,通过核心参数的逐层传递,就能很便捷支持自定义的中间件

相关推荐
一斤代码39 分钟前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子42 分钟前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年44 分钟前
从前端转go开发的学习路线
前端·学习·golang
中微子1 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina1 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路2 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_2 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说2 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409193 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding3 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js