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

相关推荐
IT乐手16 分钟前
48队都装不下你|国足第24次让全世界失望
前端
SoaringHeart1 小时前
Flutter最佳实践:IM聊天文字链接自动识别跳转
前端·flutter
掘金一周2 小时前
企业中要做智能体,最佳的方案是什么? | 沸点周刊 6.18
前端·人工智能·ai编程
Darling噜啦啦2 小时前
CSS 3D 变换与 Flex 布局实战:从零打造旋转立方体
前端·css
十九画生2 小时前
parentID ``` JavaScript 是区分大小写的,所以这两个不是同一个字段。 第二,`parent` 没有声明。 应该先写: `
javascript
秃头网友小李2 小时前
前端难点:keep-alive 缓存什么?RouterView 的 key 为什么要带 scopeId?
前端·vue.js
鱼人2 小时前
CSS 变量:一个变量救你一百次复制粘贴
前端
长大19882 小时前
CSS 到底是什么?和 HTML 的区别一次讲清楚
前端
禅思院2 小时前
路由性能优化终极指南:从懒加载漏洞到边缘渲染的架构跃迁
前端·架构·前端框架
怕浪猫2 小时前
Electron 开发实战(十六):总结与展望|生态现状、框架对比、行业趋势与学习指南
前端·javascript·electron