Zustand-天生为了React设计的状态管理工具+源码分析

6,231 阅读13分钟

前言

作为React坚实的拥护者,从v16开始,历经class到hook的完整演变。

当然React作为一个纯粹的Js Library,并没有提供状态管理的api,对于状态管理这个命题来说,社区以及React都给出了不同的答案。

笔者历经Redux,dva,mobx,每一次有更为先进的解决方案,我都争先做吃螃蟹的人,率先在项目中引进并且实践,而这一次,非常高兴zustand给我带来惊喜。

我甚至可以很负责任的说,Zustand,它是一个天生为了React服务而诞生的状态管理工具。

状态管理的本质

笔者认为,任何的状态管理工具,都离不开两个本质要素:

1.单例对象(后面简称:store)持有状态/数据,并且对外提供增删改查

2.观察者模式,即store的状态变化了,通过事件通知告诉使用者最新的状态

基于这两个要素,我可以用以下伪代码实现

function CreateStore(initState) {
    const listeners = []; // 监听事件
    const state = initState || {}; // 单例
    const setState = (newState) => {
        // xxx 修改state 并且触发监听事件
        listeners.forEach((listener) => listener(newState, state));
        state = newState;
    };
    const getState = () => state;
    const describe = (f) => {
        // 将事件推入listeners
        // 对外提供取消监听方法
        return () => xxxxx
    }

    return {
        setState,
        getState,
        describe
    }
}

是的,只要满足了这两个基本要素,你甚至可以自己设计手写一个简单版的Store工具。

对于不同的库来说,基本也是围绕这两个基本点出发加以设计。

这里以Redux为例子

Redux核心也是通过单例+观察者,但redux对于如何setState 有着自己的哲学

redux哲学贯彻 state -> dispatch -> action -> reducer -> new State -> listeners

对于熟练的Redux的开发者来说,这个链条会非常熟悉,redux的开发者Dan,也是React的核心团队之一,这种约束型的setState简直和react的单向数据流如出一辙,以至于React甚至自己在hook版本里,也新增了useReducer。

所以笔者再次总结和重申:任何的Store管理库无外乎都是以上2要素的结合,而不同库的实现细节多是怎么在setState这里加以设计,比如如何性能优化,以及插件设计。这点非常重要,这对于我们如何对库进行学习,以及如何进行理解会起到很大的帮助。

接下来迎来我们的重头戏,Zustand的源码解析。

Zustand

First create a store 首先来创建个简单的store 我们把github上官网的例子拿来

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

Zustand非常舒适的一点,是完全不需要connect,provider,直接如hook一样在我们的react组件中就能使用

function Hello() {
    const {bears, increasePopulation, removeAllBears} = useBearStore();
    return <div>
        {bears}
        <button onClick={increasePopulation}>add</button>
        <button onClick={removeAllBears}>reset</button>
    </div>
}

即使是Mobx6声称自己是极简化的Api,但对比起Zustand,mobx即使不需要provider 但始终还是少不了observer&makeAutoObservable

所以我认为Zustand才是真正意义上的0 api,以及我开头所说,Zustand这种设计天生就是为了React而诞生的一般,接下来会根据上面的2段代码,分析下源码,相信你会明白我所说的

create

import { create } from 'zustand'

这个就是方法是zustand暴露出来的api,你可以把它当成createStore的作用。

他需要我们的入参形如以下这样的函数,这种设计可以说在各个有名的框架中都非常普遍,比如koa,egg,webpack

create((set, get, api) => ({xxxxx}))

首先来看create接受到这样的函数会做什么,通过拆分代码行来理解源码

const createImpl = (createState) => {
    const api = typeof createState === "function" ? createStore(createState) : createState;
    
    xxxxxxxx
    xxxxx
    xxx
};

zustand导出的create,会经由createImpl 调用createStore 这一步跟我们上面所说的,是满足两个基本要素

const createStoreImpl = (createState) => {
    let state;
    const listeners = /* @__PURE__ */ new Set(); // 用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 : Object.assign({}, state, nextState);
            listeners.forEach((listener) => listener(state, previousState)); // 触发观察者事件,
        }
    };
    const getState = () => state;
    const subscribe = (listener) => {
        // 将事件推入listeners
        // 对外提供取消监听方法
        listeners.add(listener); 
        return () => listeners.delete(listener);
    };
    const destroy = () => {
        // 清除所有event
        listeners.clear();
    };
    const api = { setState, getState, subscribe, destroy }; // api对象提供给外部
    state = createState(setState, getState, api); // 将(set, get, api) => ({xxxxx})参数的函数执行,初始化state
    return api;
};

到这里为止,zustand所做的跟我们前面所说的大致相同,也是通过函数闭包,加以单例+观察者模式。

对比redux实现不同的关键点在于双方的setState不一样,zustand的做法是将修改state的过程完全抽象化,新的state 通过partial(state)去产生,而这个partial函数则可大有所为,在函数示编程的领域内,通过compose不同的函数来合成这个partial,就实现了一个中间件/插件机制。

相比于zustand完全抽象化修改state这一过程,redux则是通过强约束dispatch -> action -> reducer这条脉路来完成setState这一过程。

how convert store into hook

通过上面的学习,我们现在已经知道,zustand-create的第一步,就是createStore,也就是构建状态管理基本2要素-单例&观察者

const createImpl = (createState) => {
    
    // 基本2要素
    const api = typeof createState === "function" ? createStore(createState) : createState;  
   
};

但是要完全跟框架react结合,以及useBearStore为什么能像hook一样能在react中使用呢,原因在于zustand-create 本质是个高阶函数,函数返回函数,返回的函数内部使用了react的原生hook

const createImpl = (createState) => {
    // 基本2要素
    const api = typeof createState === "function" ? createStore(createState) : createState;
    
    // 构建自定义hook
    const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn);
};

既然zustand的设计,是要面向react提供hook式api,他的核心就在于(xxx) => useStore(xxx),这一步将基本两要素也就是api,跟react关连起来,这一步起的作用,打个比方,就类似于react-redux的connect,以及mobx的oberserve,将外部store与react的更新机制进行connect,store更新了,则通知react进行render,原理皆是如此。

话不多说,看看useStore是如何做到connect reaact with zustand的store

function useStore(api, selector = api.getState, equalityFn) {
    const slice = useSyncExternalStoreWithSelector(
        api.subscribe,
        api.getState,
        api.getServerState || api.getState,
        selector,
        equalityFn
    );
    return slice;
}

useStore的内部逻辑非常简单,就是一个useSyncExternalStoreWithSelector,这是一个zustand自己实现的自定义hook,融入了一些selector逻辑,这部分逻辑主要是为了性能优化有关,跟我们理解zustand源码关系不大,先不讲解selector

zustand实现的这个useSyncExternalStoreWithSelector内部最为关键的是React的useSyncExternalStore hook。

image.png

这个hook接受zustand的store的添加观察者,以及getState~也就是在这里,把store跟react的更新机制进行connect了~

useSyncExternalStore

useSyncExternalStore大家可能会很陌生,因为不常用到这个东西。甚至包括React官方都说,不希望开发者自己使用这个hook,这个hook是为了提供给一些库开发使用的。

在这里,我想先解释一下useSyncExternalStore的名字,来帮助大家增加对他的理解,即使我们对这个hook的内部逻辑一无所知,也可以理解这个hook是干啥的

理解这个hook 可以拆分他的名字: useSyncExternalStore

use - react推崇的hook 马甲

Sync - 同步,也指这个hook发起的更新是同步更新

External - 外部

Store - 数据状态(管理)

所以合起来,很好理解!useSyncExternalStore就是使用外部数据状态并且状态更新后能发起一个同步更新的hook!!!

到此为止,zustand的原理就很清晰明了:

1.实现状态管理基本2要素:单例store+观察者 2.使用React内置的hook - useSyncExternalStore,将store跟react进行关联

所以为了实现connect,想也知道,react必须拿到store的观察者以及getState嘛 此时,尽管我们还不知道useSyncExternalStore源码实现,但我们已经清晰的知道这个hook是干什么的,如果你还有兴趣和耐心,可以往下看看源码,如果你赶时间,掌握到这里也已经够了。

如果还有求知欲,可以再来看看源码,可能这真的会很枯燥


// react useSyncExternalStore
useSyncExternalStore: function (subscribe, getSnapshot, getServerSnapshot) {
        currentHookNameInDev = 'useSyncExternalStore';
        mountHookTypesDev();
        return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},

关键是这个mountSyncExternalStore,为了方便理解,会去掉一些无关代码

function mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
    var fiber = currentlyRenderingFiber$1; // 当前fiber
    var hook = mountWorkInProgressHook(); // create a new hook 来保存api

    if (isHydrating) {
        // 服务端ssr 不需要看
    } else {
        nextSnapshot = getSnapshot(); // 拿到一次store的数据快照
        var root = getWorkInProgressRoot(); // 根fiber
    } 


    hook.memoizedState = nextSnapshot; // 保存state
    var inst = {
        value: nextSnapshot,
        getSnapshot: getSnapshot
    };
    hook.queue = inst; // hook存储 queue

    mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]); 

    fiber.flags |= Passive;
    pushEffect(HasEffect | Passive$1, updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot), undefined, null);
    return nextSnapshot;
}

经过删减的react代码,这里我们只讨论客户端的情况,应该不难理解,我会逐步解释

mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]); 

这一步代码之前的逻辑,就是常规的hook逻辑,将 subscribe, getSnapshot保存在hook结构中,同时初始化一次state进行保存。

subscribeToStore会做什么呢?

function subscribeToStore(fiber, inst, subscribe) {
    var handleStoreChange = function () {
        if (checkIfSnapshotChanged(inst)) { // objectIs算法浅比较
            // Force a re-render. 这里就是强制更新react
            forceStoreRerender(fiber);
        }
    }; // Subscribe to the store and return a clean-up function.


    return subscribe(handleStoreChange); // 返回一个解除监听的调用
}

这里看我的注释就明白了,当然,这个函数名也相当的言简意赅,就是监听外部的Store,不过,大概率你会问mountEffect是干什么的,他其实等同于以下代码:

useEffect(() => {
    return subscribeToStore(xxxxx);
}, []);

这个可能就是大家很熟悉的画面了。接着继续看到最后一点点源码了

pushEffect(HasEffect | Passive$1, updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot), undefined, null);

updateStoreInstance,看函数的名字也知道,就是更新对着我们的store来更新,而且看源码他也是这么做的

function updateStoreInstance(fiber, inst, nextSnapshot, getSnapshot) {
    inst.value = nextSnapshot;
    inst.getSnapshot = getSnapshot; 

    if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceStoreRerender(fiber);
    }
}

雄关漫道真如铁,恭喜你终于完成了所有的源码阅读。

所以我们来总结一下useSyncExternalStore

1.作用是用外部数据状态并且状态更新后能发起一个同步更新的hook

2.内部源码实现则是初始化/更新时,用hook结构保存我们的state&观察者 并且使用的是调度任务(副作用)你可以理解是在源码层面使用了2次useEffect

前一次Effect来注册观察者任务发起更新,后一次任务则是直接来一次比较更新

所以从源码上来说,zustand的store是经过了react两次,第一次调用set,触发了观察者中的subscribeToStore的更新,这一次更新完成了old state -> new State

第二次则是由updateStoreInstance发起的更新,这次更新会根据第一次获得的new State来生成我们的ui~

函数or对象

回到我们的zustand-create

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))
const createImpl = (createState) => {
    // 基本2要素
    const api = typeof createState === "function" ? createStore(createState) : createState;
    
    // 构建自定义hook
    const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn);
};
function Hello() {
    const {bears, increasePopulation, removeAllBears} = useBearStore();
    return <div>
        {bears}
        <button onClick={increasePopulation}>add</button>
        <button onClick={removeAllBears}>reset</button>
    </div>
}

正是由于zustand这样的实现,store就则样天然的作为了hook实现了store。

但是对于react之外的一些地方,zustand甚至也实现了非hook调用

const bearsStore = useBearStore.getState();

这部分即是函数又是对象的实现,原因在于js函数类型本质也是对象类型,所以可以对函数进行api扩展

Object.assign(useBoundStore, api);

所以,zustand的create 全部源码如下:

const createImpl = (createState) => {

    // 构建基本2要素
    const api = typeof createState === "function" ? createStore(createState) : createState;

    // 将2要素跟react更新相connect
    const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn);

    // 将函数作为对象进行api扩展,方便非react环境调用
    Object.assign(useBoundStore, api);
    return useBoundStore;
};

一些思考

经过我们上面的学习,我想大家多少应该理解开篇我所说的那句话以及我的标题

"我甚至可以很负责任的说,Zustand,它是一个天生为了React服务而诞生的状态管理工具"

没错,zustand就是这样小而美又充满了设计感的状态管理库

得益于他已经天生内置了对react的connect,使他自带了对react的hook支持,以及这种react/非react的全面支持性

他确实天生为此而来

如果真要拿他跟Redux相比,相反,我更认为Redux才是真正血统纯正的数据管理库,他本身对于state的派生哲学,以及不跟任何的库进行耦合

但如果你要说谁更适合React,我认为没有统一的答案,但就我个人而言,我更喜欢zustand

不足之处

hook概念跟react的哲学是有一定的冲突的

熟练的开发者都知道,hook只能用于func component body内,此外其他的地方使用hook都会报错,但zustand这种useStore.getState(),这样的设计却可以出现在循环/事件/条件判断中,非常容易跟react hook的概念冲突,这对于新手开发者来说就不太友好了

需要更友好的表层机制

zustand这种flux 函数示的设计,使得中间件机制非常容易实现,但是zustand表层使用并没有提开发者节省心智负担,比如官方文档下的例子

import { create } from 'zustand'
import { persist, createJSONStorage, devtools, immer } from 'zustand/middleware'

const useFishStore = create(
    devtools(
        immer(
            persist(
                (set, get) => ({
                    fishes: 0,
                    addAFish: () => set({ fishes: get().fishes + 1 }),
                }),
                {
                    name: 'food-storage', // unique name
                    storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
                }
            )
        ),
        { name, store: storeName1 }
    )
)

就我个人的使用心得来说,这种叠罗汉式的写法非常容易搞得摸不着头脑,改一点东西需要小心翼翼,心智负担着实不低

所以我自己将immer,devtools,persist插件整合改造了createStore函数,让我们开发者的心智只集中在我们需要的store本身

import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'
import flip from "lodash/flip";
import toArray from "lodash/toArray";

const createFlipFn = fn => {
    const flipped = flip(function() {
        const args = toArray(arguments);
        return fn(...args); 
    });
    return flipped;
}

type InitialState<T> = (set: any, get: () => T) => T;
type UpdateStore<T> = T & {
    update: (updater: (config: T) => void) => void;
}

export default function createZustandStore<T>(constructor: InitialState<T>, config = {} as any) {
    const fp = [immer];

    if (config.persistConfig) {
        fp.push(createFlipFn(persist).bind(null, config.persistConfig));
    }

    if (config.devtoolsConfig) {
        fp.push(createFlipFn(devtools).bind(null, config.devtoolsConfig));
    }

    const addUpdater = (set, get) => {
        const _ = constructor(set, get) as UpdateStore<T>;
        _.update = updater => {
            const config = { ...get() };
            updater(config);
            set(() => config);
        };
        return _;
    }


    return create<UpdateStore<T>>()(fp.reduce((prev: any, next) => {
        return next(prev);
    }, addUpdater) as any);
}

这里的原理是用了函数式编程的颗粒化和反转,组合的概念,对于我们开发者就可以这么使用zustand了

export const useCounter = createZustandStore<Counter>((set, get) => ({
    count: 0,
    add: () => set((state) => { 
        state.count += 1 
    }),
    del: () => set((state) => { state.count -= 1 }, false, "dec counter"),
    double: () => set((state) => ({ count: state.count + 2 })),
}), {
    persistConfig: { name: "counter-storage" },
    devtoolsConfig: { name: 'counterLog' }
});

如果不需要对应的插件,直接去掉config字段即可,如此一来,我们的开发者将能更好维护一个pure store

结尾

作为总爱先吃螃蟹的人来说,总是在第一时间引入一些新的东西

而这一次zustand确实让我感觉惊喜,可能我会在未来很长一段时间将它作为项目中的稳定的主力军。

不过时代发展真的很快,技术更迭甚至是没有上限的概念,深感自己学不完学不到头 没办法。

当我从redux不断过度,向dva,mobx进行技术革新

而我一度曾坚定的认为mobx6+react就是react对于数据状态管理的终极命题答案时

zustand又狠狠得打了我的脸~

哈哈哈,而这一次,我再也不断言zustand是最完美的react的store解决方案了

但就我的视角来说,他真的是生而为了React所诞生的一般!

感谢看到这里的读者,感谢你们的耐心~