一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
1. zustand 及api介绍
一个简单的类flux的数据管理库。为什么说是类flux,因为zustand的核心概念 单一数据来源(one source of truth) 是跟flux的数据概念相似的。这里有flux概念介绍。
zustand是有基于hooks实现的api,但并不是所有api都是基于hooks,这也是为什么可以不依赖react去单独使用。但是zustand与react一起使用时极其舒适。
主要api就一个:
create: 构建store
const useStore = create((set, get) => ({
bears: 0,
increase: () => set(state => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 })
}))
然后可以这样使用useStore
function BearCounter() {
const bears = useStore(state => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increase = useStore(state => state.increase)
return <button onClick={increasePopulation}>one up</button>
}
2. 源码分析
如果从入口文件开始,一个函数一个函数的分析也可以。但是我比较喜欢带着问题去找答案,所以给自己提问,然后去源码中找答案。
2.1 从 create 开始
2.1.1 问题 1: 入口函数create构建的时候,接受了一个函数(createState)作为参数,签名如下,这里为什么set可以修改状态,而get可以获取所有状态,api可以用来干嘛?
// createState
(set, get, api) => { // ... }
create这里会根据当前传入的是否为函数来决定是调用系统默认的createStore函数直接使用自定义createStore。
// https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts
const createStore = (createState) => {
let state
const listeners = new Set()
const setState = () => {}
const setState = () => {}
const subscribe = () => {}
const destroy = () => {}
const api = { setState, getState, subscribe, destroy }
// 传入的函数会在这里调用,同时传入set,get,api作为参数
state = createState(set, get, api)
return api
}
const setState = (partial, replace) => {
// partial可以直接传入新的状态,也可以是函数的返回值
const nextState = typeof partial === 'function' ? partial(state) : partial;
// 浅比较,如果不同再做更新
if (nextState !== state) {
// 如果是replace就直接替换,否则混入到之前的状态并用新状态替换之前的状态
state = replace ? nextState : Object.assign({}, state, nextState);
// 调用所有监听器。(这里listen是在subscribe中注册的。)
listeners.forEach(listener => listener(state));
}
};
// 利用闭包,保存state状态,get时直接返回
const getState = () => state;
api中暴露了状态里的主要方式。
2.1.2 答案: 主要是利用闭包:维护一个内部状态state,set去update,get则是返回state
2.2 create()
2.2.1 问题2:create()调用之后,有时候用store接收返回值,有时候又用useStore接收返回值是怎么回事?
// 简化一下create
const create = (createState) => {
// 这里的api就是上面createStore的返回值
const api = typeof createState === 'function' ? createStore(createState) : createState
// 定义useStore函数
// 这里是基于react的hooks封装了 useStore 方法,用在react中
// 其他场景直接使用api
const useStore = (selector, equalityFn) => {}
// 将api里的方法作为useStore的方法
Object.assign(useStore, api)
return useStore
}
2.2.2 看到这段就很明显,在create函数中,将createState的返回值作为了useStore的属性。这样在react的函数组件内把useStore作为hook使用;在函数外部或者非react环境,可以定义store = create(); { getState, setState, subscribe... } = store去使用zustand。
2.3 useStore
2.3.1 问题3: useStore是怎么工作的?源码
const useStore = (selector = api.getState, equalityFn = Object.is) => {
const [, forceUpdate] = useReducer(c => c + 1, 0);
const state = api.getState();
const stateRef = useRef(state);
const selectorRef = useRef(selector);
const equalityFnRef = useRef(equalityFn);
const erroredRef = useRef(false);
const currentSliceRef = useRef();
if (currentSliceRef.current === undefined) {
currentSliceRef.current = selector(state);
}
let newStateSlice;
let hasNewStateSlice = false;
// 在监听器中已修改了stateRef.current = state 所以大部分情况是不会走进这个if语句里的
if (stateRef.current !== state || selectorRef.current !== selector || equalityFnRef.current !== equalityFn || erroredRef.current) {
// Using local variables to avoid mutations in the render phase.
newStateSlice = selector(state);
hasNewStateSlice = !equalityFn(currentSliceRef.current, newStateSlice);
} // Syncing changes in useEffect.
// 兼容SSR,有window就用useLayoutEffect,否则就用useEffect
useIsoLayoutEffect(() => {
if (hasNewStateSlice) {
currentSliceRef.current = newStateSlice;
}
stateRef.current = state;
selectorRef.current = selector;
equalityFnRef.current = equalityFn;
erroredRef.current = false;
});
const stateBeforeSubscriptionRef = useRef(state);
useEffect(() => {
const listener = () => {};
const unsubscribe = api.subscribe(listener);
return unsubscribe;
}, []);
return hasNewStateSlice ? newStateSlice : currentSliceRef.current;
};
2.3.2 答案:这里其实就干了3件事情
-
- 缓存全局所有状态state,以及通过
selector获取的局部状态
- 缓存全局所有状态state,以及通过
-
- 在
useEffect中通过api中提供的subscribe注册监听器。这里就是典型的观察者模式。
- 在
try {
// 获取下一个 所有状态,
const nextState = api.getState();
// 获取 局部状态
const nextStateSlice = selectorRef.current(nextState);
// 比较与上一次的状态是否有变化,如果有变化,强制更新
if (!equalityFnRef.current(currentSliceRef.current, nextStateSlice)) {
stateRef.current = nextState;
currentSliceRef.current = nextStateSlice;
forceUpdate();
}
} catch (error) {
erroredRef.current = true;
forceUpdate();
}
-
- 强制更新会导致
useStore重新执行,然后获取最新的局部变量
- 强制更新会导致
注册监听器 --> 更新状态 --> 比较局部状态是否变化 --是--> 更新状态
|
|
--否--> 不变化
2.4 middleware
2.4.1 问题4: zustand为什么可以像👇🏻 这样使用middleware呢?
// Log every time state is changed
const log = config => (set, get, api) => config(args => {
console.log(" applying", args)
set(args)
console.log(" new state", get())
}, get, api)
// Turn the set method into an immer proxy
const immer = config => (set, get, api) => config((partial, replace) => {
const nextState = typeof partial === 'function'
? produce(partial)
: partial
return set(nextState, replace)
}, get, api)
const useStore = create(
log(
immer((set) => ({
bees: false,
setBees: (input) => set((state) => void (state.bees = input)),
}))
),
)
首先,根据上面的分析,可以知道
-
create接受一个函数createState作为参数,且函数签名是这样的(set, get, api) => { // ... },所以只要我们在调用create时,最终的输入是符合上述签名的函数就可以。
-
createState的返回值是store中的数据来源state
所以我们可以利用高阶函数,在createState外层封装其他功能,只要保证签名和返回值与最初的结果一致就可以。
不考虑middleware情况:
const stateConfig = (set, get, api) => ({
bees: false,
setBees: (input) => set((state) => void (state.bees = input)),
})
const useStore = create(stateConfig)
// log调用 保证 签名一致
const log = (stateConfig) => {
// 高阶函数内部调用stateConfig 保证 返回值一致
return function(set, get, api) {
return stateConfig(args => {
console.log('')
set(args)
console.log('')
}, get, api)
}
}
而immer的场景,是在上面的基础之上,重写了setState。
如果有业务需要,也可以对getter甚至api进行扩展。
2.4.2 答案:只是高阶函数的使用
2.5 subscribe
2.5.1 subscribe是如何工作的?
2.5.2 回答: 这里subscribe就是实现了一个观察者模式了。set类型的listerers负责收集监听器,在setState的时候回去触发所有的listeners。
const subscribeWithSelector = (listener, selector = getState, equalityFn = Object.is) => {
let currentSlice = selector(state);
function listenerToAdd() {
try {
const newStateSlice = selector(state);
// 比较当前slice的状态是否变化
if (!equalityFn(currentSlice, newStateSlice)) {
listener(currentSlice = newStateSlice);
}
} catch (error) {
listener(null, error);
}
}
listeners.add(listenerToAdd); // Unsubscribe
// 取消,并删除监听器
return () => listeners.delete(listenerToAdd);
};
const subscribe = (listener, selector, equalityFn) => {
if (selector || equalityFn) {
return subscribeWithSelector(listener, selector, equalityFn);
}
listeners.add(listener); // Unsubscribe
return () => listeners.delete(listener);
};
从源码可以看出来,在subscribe传入了selector或者equalityFn时,表示的是局部获取数据状态,这时如果内部状态变化,需要比较所依赖的数据是否有变化,通过equalityFn比对之后再去更新,这样就能做到局部更新。
3. 总结
zustand主要使用观察者模式 + hook 实现的一个状态管理库,利用比较函数做到局部更新。