更好的阅读体验: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
调用。
- setState_set -> A_createState
- A_set(setState_set) -> B_createState
- B_set(A_set) -> C_createState
- C_set(B_set) -> config_createState
这样调用链路就清晰了。
当 config_createState
内执行了 set 的时候,就是从 C_set 开始执行,往上追溯到 B_set、A_set、setState_set。
结论:从最内层的中间件开始逐层往外执行。
所以,当调用 set 的时候,输出顺序如下:
- C-1
- B-1
- A-1
- Vanilla
- A-2
- B-2
- C-2
结语
zustand 的中间件从功能上还是很简单的,通过核心参数的逐层传递,就能很便捷支持自定义的中间件