Zustand 源码阅读计划(1)- JS 篇 - Vallina(框架无关)逻辑
源码内部是 TS 的,我们暂时先只关注最核心的 JS 功能实现 你问我怎么只看 JS ?当然是看编译后的产物啦
完整代码
'use strict';
const createStoreImpl = (createState) => {
let state;
const listeners = /* @__PURE__ */ new Set();
const setState = (partial, replace) => {
const nextState = typeof partial === "function" ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
const getState = () => state;
const getInitialState = () => initialState;
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const api = { setState, getState, getInitialState, subscribe };
const initialState = state = createState(setState, getState, api);
return api;
};
const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
exports.createStore = createStore;
设计思路
首先,我们不要为了看源码而看源码,知其然不知其所以然,源码是看懂了,但全然不知道为什么这么设计的,这也不行。
Zustand
本质是什么?是一个状态管理库,不管它再怎么样,实现的都是管理状态的。那么要管理状态,需要哪些功能?最需要的当然是记录当前状态,这是最重要的功能。有了记录了,我们还需要去获取,所以还需要获取当前状态的功能。状态从哪来呢?那就还需要修改当前状态的功能。
那么我们可以得出结论,要实现一个状态管理库,那么必须要有三个功能
- 记录存储当前的状态
- 获取当前最新的状态
- 修改当前存储的状态
初步源码解析
知道核心功能了,我们对照着源码来看看。
'use strict';
const createStoreImpl = (createState) => {
// 1️⃣
let state;
// 2️⃣
const setState = (partial) => {
state = Object.assign({}, state, partial)
};
// 3️⃣
const getState = () => state;
// 4️⃣
const api = { setState, getState };
state = createState(setState, getState, api);
return api;
};
const createStore = (createState) => createStoreImpl(createState);
exports.createStore = createStore;
我们简化了一下源码,去掉了一些工程上便利的东西,只保留了最核心的逻辑。
可以看到
1️⃣ 定义了一个 state
变量,用以记录存储当前的状态;
2️⃣ 定义了一个 setState
方法,用以修改当前存储的状态
3️⃣ 定义了一个 getState
方法,用以获取当前最新的状态
4️⃣ 将 setState
和 getState
聚合到 api
中,创建 store
的时候就会将这两个方法返回出去,这样我们就可以随处调用这两个方法来获取状态或者修改状态了
这样,一个最简单的状态管理库就实现啦! 是不是很简单
我们来使用一下

最小实现示例
当然,还未完待续,我们继续深入看看刚才被简化掉的内容,作用是什么。
发布订阅
除了我们主动获取最新数据以外,很多时候我们还需要被动通知。当状态发生变化的时候,需要通知每一处需要同步变化的地方。即,发布订阅模式。
我们提取一下发布订阅的核心逻辑
const createStoreImpl = (createState) => {
let state;
// 1️⃣
const listeners = /* @__PURE__ */ new Set();
const setState = (partial, replace) => {
const nextState = partial;
const previousState = state;
state = Object.assign({}, state, nextState);
// 2️⃣
listeners.forEach((listener) => listener(state, previousState));
};
const getState = () => state;
// 3️⃣
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const api = { setState, getState, subscribe };
state = createState(setState, getState, api);
return api;
};
const createStore = createStoreImpl(createState);
exports.createStore = createStore;
在上面的最小实现的基础上,添加了发布订阅的相关逻辑。
1️⃣ 定义了事件收集器,在这里用了 set 来做去重(记得通常是要先定义函数再传入,而非直接传入箭头函数。直接传入箭头函数无法去重,且 set 取消订阅时删除的效率高)。/* @__PURE__ */
的标记是给打包工具看的,它的作用是告诉这些工具:“这个函数调用是‘纯粹的’(pure),没有副作用(side-effects)。”。
如何理解这句话?这会影响到打包过程中的 tree-shaking
流程,当打包工具认为代码可能存在副作用(在这里是 set 定义与否会影响到其他地方使用,如果贸然删除会导致报错)。而声明了 /* @__PURE__ */
,就等于告明打包工具,如果它判断该变量没有使用到,那么完全可以删除掉这个定义。
例如当用户完全没有使用到 subscribe 的时候,最终代码里也没必要保留相关的逻辑。
2️⃣ 当前状态发生变更时,通知所有的订阅者,将最新的状态和变化前的状态都传给订阅回调函数。在这里任何变化都会通知,但其实实际上我们订阅的时候只关注某些内容,这个留待后续讲解。
3️⃣ 订阅通知,传入一个回调函数,添加到事件中心,并返回一个取消监听的方法。
复杂判断
回看完整代码,显然还是比我们的实现复杂很多,那么复杂在哪里了呢。
1. setState
const nextState = typeof partial === "function" ? partial(state) : partial;
setState
的 partial
部分是可以传值或者传一个函数进去的。而我们需要的是一个值,所以需要对传入的内容进行处理。
首先就是判断是否为函数,如果是函数,则调用,传入完整的 state
进去,由该函数返回需要变化的对象值。如果不是函数,那么就是变化的对象值,直接记录。
2. 创建 store
const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
createStore
这里你需要对 zustand 熟悉一点,这里的两种方法是为了对中间件做更好的类型支持,现在我们只讲 js 的部分,所以可以忽略。这个地方只支持传入回调函数都是可以的。即
const createStore = (createState) => createStoreImpl(createState)
3. 计算更新后的状态
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
这里又出现了新的东西,也就是 replace
参数,这个参数是用来表达是否需要完全替换当前状态,这个功能适用于同步后端最新数据,强制刷新,恢复某个状态等情况。
我们来分解一下这个复杂表达式。首先是大框架
(replace != null ? replace : T) ? nextState : Object.assign({}, state, nextState)
这个意思就是如果 replace
传递了值,那就看 replace
是 true
or false
。
- 如果是
true
,那么就开启替换模式,将变化的状态直接赋值给state
。 - 如果是
false
,那么就是合并模式,将变化的状态覆盖原本的state
,创建到一个新对象中。
如果 replace 就是没传递值呢?那就取决于这块内容了
typeof nextState !== "object" || nextState === null
判定 nextState
如果不是对象,或者 nextState 是 null,那就没法和 state
合并,所以直接使用 nextState
替换当前状态。这个时候你可能有点疑问,什么时候会这个样子?
那答案当然是,setState(null)
啦
性能优化
setState
中的 !Object.is(nextState, state)
如果完全没变化,那就没必要更新状态并通知所有监听者
SSR(服务端渲染) 支持
服务端渲染的时候,需要有个直接就能获取到的初始的值,以便预渲染页面内容。
'use strict';
const createStoreImpl = (createState) => {
let state;
const listeners = /* @__PURE__ */ new Set();
const setState = (partial, replace) => {
const nextState = typeof partial === "function" ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
const getState = () => state;
// 1️⃣
const getInitialState = () => initialState;
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
// 2️⃣
const api = { setState, getState, getInitialState, subscribe };
// 3️⃣
const initialState = state = createState(setState, getState, api);
return api;
};
const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
exports.createStore = createStore;
1️⃣ 此处定义了一个函数,返回初始的状态
2️⃣ 将 getInitialState
方法暴露出去以供调用
3️⃣ 预执行传入的createState
函数,返回一个初始的状态值。将这个初始的状态值赋值给 state
和 initialState
。
结语
框架无关的部分就到此结束了,在本篇文章里,我们从最小实现开始逐步理解并实现状态管理的本质、核心、易用的逐渐外扩。
在下一章节中,我们将学习 JS 版的 react 框架适配代码。