如何使用
先来简单看看如何使用
1import { create } from 'zustand'
2
3const useStore = create((set) => ({
4 count: 1,
5 inc: () => set((state) => ({ count: state.count + 1 })),
6}))
7
8function Counter() {
9 const { count, inc } = useStore()
10 return (
11 <div>
12 <span>{count}</span>
13 <button onClick={inc}>one up</button>
14 </div>
15 )
16}
这是官网给我们的一个例子
我们可以看到他是用一个create来创建一个store的
之前我已经搭建好了调试环境zustand调试 直接开看🤫,很易读大家可以跟着文章一起来调试着看
为了方便大家看我帮大家剔除了源码中的ts,源码中的ts还是蛮复杂的🙃
创建store
可以看到create接受一个createState就是我们之前的传入的函数
1export const create = ((createState) =>
2 createState ? createImpl(createState) : createImpl)
因为我们传入的createState是个函数所以会走到里createImpl(createState)继续看下去 参道参道 我们先不关注后面的代码,我们可以看到由于我们传的createState是个函数它又调用了createStore这个函数
1const createImpl = (createState) => {
2 const api =
3 typeof createState === 'function' ? createStore(createState) : createState
4
5 const useBoundStore: any = (selector?: any, equalityFn?: any) =>
6 useStore(api, selector, equalityFn)
7
8 Object.assign(useBoundStore, api)
9
10 return useBoundStore
11}
createStore如下,很熟悉是不是,感觉和俄罗斯套娃一样🙃
1export const createStore = ((createState) =>
2 createState ? createStoreImpl(createState) : createStoreImpl)
继续往下走就看到了核心逻辑createStoreImpl,好长啊,别急慢慢看,我们分段来看
1const createStoreImpl: CreateStoreImpl = (createState) => {
2 let state;
3 const listeners = new Set();
4
5
6 const setState = (partial, replace) => {
7
8 const nextState =
9 typeof partial === 'function'
10 ? partial(state)
11 : partial
12
13 if (!Object.is(nextState, state)) {
14 const previousState = state
15 state =
16
17 replace ?? (typeof nextState !== 'object' || nextState === null)
18 ? (nextState as TState)
19
20 : Object.assign({}, state, nextState)
21
22 listeners.forEach((listener) => listener(state, previousState))
23 }
24 }
25
26 const getState = () => state
27
28 const getInitialState= () =>
29 initialState
30
31 const subscribe = (listener) => {
32 listeners.add(listener)
33
34 return () => listeners.delete(listener)
35 }
36
37 const destroy = () => {
38 listeners.clear()
39 }
40
41 const api = { setState, getState, getInitialState, subscribe, destroy }
42 const initialState = (state = createState(setState, getState, api))
43 return api
44}
我们可以看到他先创建了一个state,一个listener
然后看一个函数setState,我们好好看看这个函数
1
2 const setState = (partial, replace) => {
3
4 const nextState =
5 typeof partial === 'function'
6 ? partial(state)
7 : partial
8
9 if (!Object.is(nextState, state)) {
10 const previousState = state
11 state =
12
13 replace ?? (typeof nextState !== 'object' || nextState === null)
14 ? (nextState as TState)
15
16 : Object.assign({}, state, nextState)
17
18 listeners.forEach((listener) => listener(state, previousState))
19 }
20 }
21
我们看到获取了nextState,也很好理解,就是下一个如果parital是个函数就调用,不是就直接赋值,然后判断 nextState,state是否一样,是一个浅层比较,如果一样就证明没有发生更新,直接跳过,这里的state就是之前的 state,因为一直保存在函数闭包里。
然后核心比较如果replace传入true那直接赋值就好,就直接落入了第一个逻辑,否则在看后面的条件 typeof nextState !== ‘object’ || nextState === null 是不是基础类型,如果是同样直接赋值,如果不是就 用Object.assign({}, state, nextState)进行一个浅层合并后赋值,然后再用 listeners 发布订阅消息,使react更新
继续看下面两个函数,获取当前的状态和初始值,值得注意的是initialState在后面被赋值
1 const getState = () => state
2
3 const getInitialState = () =>
4 initialState
然后就是订阅subscribe和取消订阅,也很简单 subscribe就是把它加入set中然后同时返回一个取消订阅的函数,
destroy在后续会被舍弃,其实差不多就是一个清空的操作
1 const subscribe = (listener) => {
2 listeners.add(listener)
3
4 return () => listeners.delete(listener)
5 }
6
7 const destroy = () => {
8 listeners.clear()
9 }
最后我们把之前创建的这些函数(setState,getState,subscribe…)作为api返回出去
同时初始化一下state和initialState
1 const api = { setState, getState, getInitialState, subscribe, destroy }
2 const initialState = (state = createState(setState, getState, api))
3 return api
回到之前我们看到我们获取到了api,接着就应该接入react了
1const createImpl = <T>(createState: StateCreator<T, [], []>) => {
2
3 const api =
4 typeof createState === 'function' ? createStore(createState) : createState
5
6 const useBoundStore: any = (selector?: any, equalityFn?: any) =>
7 useStore(api, selector, equalityFn)
8
9 Object.assign(useBoundStore, api)
10
11 return useBoundStore
12}
接入react
useBoundStore核心就是在useStore里让我们看看useStore
1
2export function useStore(
3 api,
4 selector,
5 equalityFn,
6) {
7 const slice = useSyncExternalStoreWithSelector(
8 api.subscribe,
9 api.getState,
10 api.getServerState || api.getInitialState,
11 selector,
12 equalityFn,
13 )
14 return slice
15}
看到其实核心就是用了useSyncExternalStoreWithSelector 这个api,是基于官方的useSyncExternalStore做的一个封装,加上了selector 和 equalityFn,这也是为什么zustand如此简洁的原因之一。
1const createImpl = (createState) => {
2 const api =
3 typeof createState === 'function' ? createStore(createState) : createState
4
5 const useBoundStore: any = (selector, equalityFn) =>
6 useStore(api, selector, equalityFn)
7
8
9 Object.assign(useBoundStore, api)
10
11 return useBoundStore
12}
最终返回一个函数同时该函数上挂载着各种订阅相关的api
触发更新
我们可以看到create之后我们就可以调用这个useStore了,可以直接取到count和inc在useStore里
1import { create } from "zustand";
2
3const useStore = create((set) => ({
4 count: 1,
5 inc: () => set((state) => ({ count: state.count + 1 })),
6}));
7
8export function Counter() {
9 const { count, inc } = useStore();
10 return (
11 <div>
12 <span>{count}</span>
13 <button onClick={inc}>one up</button>
14 </div>
15 );
16}
点击inc就会触发更新具体会执行之前的
1const setState: StoreApi = (partial, replace) => {
2 const nextState =
3 typeof partial === 'function'
4 ? partial(state)
5 : partial
6 if (!Object.is(nextState, state)) {
7 const previousState = state
8 state =
9 replace ?? (typeof nextState !== 'object' || nextState === null)
10 ? (nextState as TState)
11 : Object.assign({}, state, nextState)
12 listeners.forEach((listener) => listener(state, previousState))
13 }
14 }
需要注意一点的是在调试时发现listeners里会加上一个函数,可以猜测到这个就是触发react更新的关键,大概率是在调用useSyncExternalStoreWithSelector时加上的,由此就接入到react里可以正常更新了,因为有个函数forceStoreRender,不过本文不太深入useSyncExternalStore也就先略过了
使用selector
传入selector,你可选择你需要的state,或者进行一些计算属性
1import { create } from "zustand";
2
3const useStore = create((set) => ({
4 count: 2,
5 inc: () => set((state) => ({ count: state.count + 1 })),
6}));
7
8export function Counter() {
9 const count = useStore((state) => state.count);
10 const keys = useStore((state) => Object.keys(state))
11 return (
12 <div>
13 <span>{count}</span>
14 <button>one up</button>
15 </div>
16 );
17}
这样我们就无需解构就可以直接拿到了count,在源码中zustand做的也很简单就是将selector传给useSyncExternalStoreWithSelector
useShallow
当你需要订阅存储中的一个计算状态时,推荐的方式是使用一个selector
这个计算选择器会在输出发生变化时导致重新渲染,判断变化的方式是使用Object.is。
在这种情况下,你可能希望使用useShallow来避免重新渲染,如果计算出的值始终与先前的值浅相等的话。
一个例子,来自官方文档
1import { create } from 'zustand'
2
3const useMeals = create(() => ({
4 papaBear: 'large porridge-pot',
5 mamaBear: 'middle-size porridge pot',
6 littleBear: 'A little, small, wee pot',
7}))
8
9export const BearNames = () => {
10 const names = useMeals((state) => Object.keys(state))
11
12 return <div>{names.join(', ')}</div>
13}
我们试图更新这个store
1useMeals.setState({
2 papaBear: 'a large pizza',
3})
这个改动导致了BearNames重新渲染,即使根据浅相等的定义,names的实际输出并没有发生变化。
这时候你就可以这样用,来避免重新渲染
1import { create } from 'zustand'
2import { useShallow } from 'zustand/react/shallow'
3
4const useMeals = create(() => ({
5 papaBear: 'large porridge-pot',
6 mamaBear: 'middle-size porridge pot',
7 littleBear: 'A little, small, wee pot',
8}))
9
10export const BearNames = () => {
11 const names = useMeals(useShallow((state) => Object.keys(state)))
12
13 return <div>{names.join(', ')}</div>
14}
看看如何实现的,就是用一个ref去存储之前的值,然后进行比对就是用shallow方法,如果一样就直接返回prevent.current,如果不一样就更新prev.current
1import { useRef } from 'react'
2import { shallow } from '../vanilla/shallow.ts'
3
4export function useShallow(selector) {
5 const prev = useRef<U>()
6
7 return (state) => {
8 const next = selector(state)
9 return shallow(prev.current, next)
10 ? (prev.current)
11 : (prev.current = next)
12 }
13}
然后我们再看看shallow
- 首先用Object.is判断
- 排除掉null和基础值,前面已经判断过Object.is不符合说明这些值应该更新
- 遍历Map,Set一个值一个值进行Object.is比较
- 如果是普通的Object的就拿到键然后先比长度,长度不相等自然不相等
- 再遍历对象键值比较,先看有无该键,然后在看该键上值是否相等
1export function shallow<T>(objA: T, objB: T) {
2
3 if (Object.is(objA, objB)) {
4 return true
5 }
6
7
8 if (
9 typeof objA !== 'object' ||
10 objA === null ||
11 typeof objB !== 'object' ||
12 objB === null
13 ) {
14 return false
15 }
16
17
18 if (objA instanceof Map && objB instanceof Map) {
19 if (objA.size !== objB.size) return false
20
21 for (const [key, value] of objA) {
22 if (!Object.is(value, objB.get(key))) {
23 return false
24 }
25 }
26 return true
27 }
28
29 if (objA instanceof Set && objB instanceof Set) {
30 if (objA.size !== objB.size) return false
31
32 for (const value of objA) {
33 if (!objB.has(value)) {
34 return false
35 }
36 }
37 return true
38 }
39
40
41 const keysA = Object.keys(objA)
42 if (keysA.length !== Object.keys(objB).length) {
43 return false
44 }
45 for (let i = 0; i < keysA.length; i++) {
46 if (
47
48 !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
49
50 !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
51 ) {
52 return false
53 }
54 }
55 return true
56}
中间件
zustand有几个很好的中间件,我就先只带大家看个immer的,其他大家可以自行查看
我们先要下载一下 immer, 然后再从zustand/middleware/immer中引入immer
1import { create } from "zustand";
2import { immer } from "zustand/middleware/immer";
3
4type State = {
5 count: number;
6};
7
8type Actions = {
9 increment: (qty: number) => void;
10 decrement: (qty: number) => void;
11};
12
13const useCountStore = create<State & Actions>()(
14 immer((set) => ({
15 count: 0,
16 increment: (qty: number) =>
17 set((state) => {
18 state.count += qty;
19 }),
20 decrement: (qty: number) =>
21 set((state) => {
22 state.count -= qty;
23 }),
24 }))
25);
26export function Counter() {
27 const { count, increment } = useCountStore();
28 return (
29 <div>
30 <span>{count}</span>
31 <button
32 onClick={() => {
33 increment(2);
34 }}
35 >
36 two up
37 </button>
38 </div>
39 );
40}
我们来看源码看出导出的这个就是个immerImpl
1const immerImpl = (initializer) => {
2 return (set, get, store) => {
3 store.setState = (updater, replace, ...a) => {
4
5 const nextState = (
6 typeof updater === 'function' ? produce(updater) : updater
7 )
8 return set(nextState, replace, ...a)
9 }
10
11 return initializer(store.setState, get, store)
12 }
13}
其实就是相当于代理了之前的setState,在之前setState之前用immer的produce处理了nextState,然后正常再set就是之前setState
处理异步
在zustand里我们可以很容易处理异步,几乎无感
1import { create } from "zustand";
2
3const useCountStore = create((set) => ({
4 data: {},
5 fetch: async () => {
6 const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
7 const result = await res.json();
8 set({ data: result });
9 },
10}));
11export function Counter() {
12 const { data, fetch } = useCountStore();
13 return (
14 <div>
15 <span>{data.id}</span>
16 <button
17 onClick={() => {
18 fetch();
19 }}
20 >
21 fetch
22 </button>
23 </div>
24 );
25}
总结
zustand的设计足够简单,十分灵活,代码也很简洁,充分利用了react的hook,中间件的设计增强了该库的拓展性,十分建议大家通过此文自己去看看zustand的源代码,本文只起一个抛砖引玉的作用。
附一张网络上的原理图,不是我画的但觉得蛮好的