我用过或了解过前端业界大部分流行的状态管理库。他们有的很复杂,有的很简单。有的用了一些深度改造的手段来优化细节,有的则是平铺直叙的告诉所有使用者发生了变化。在技术方案诡异多变与层出不穷的当下,只有一个状态管理库让我深深着迷,她极度精简到让我觉得不能再简单了,但是她也足够完备到应对任何场景。而我就一直在追究这样的一个宛如艺术品一样的状态管理库,经过一段时间的使用,我很确定她就是我的梦中情库。
她的名字叫 zustand
Github: https://github.com/pmndrs/zustand
极简定义
我们先看看其他业界的状态管理库的使用方式:
以比较主流的redux
和mobx
为例, 这里直接复制了官网的最小示例。
redux(@reduxjs/toolkit)
1 | import { createSlice, configureStore } from "@reduxjs/toolkit"; |
mobx
1 | import React from "react"; |
在 redux
中,我们需要先构建 reducer
来记录如何处理状态,然后根据reducer
构建一个store
。取值是通过store
来获取,修改则需要通过 基于reducer
一一对应的action
来修改。
这样就确保了数据流永远是单向流动的: 即 UI -> action -> reducer -> state -> UI
这样的过程。其响应式的实现就是在执行action -> reducer
的过程中收集到了变化,然后通知所有订阅这个store
的所有人。然后订阅者再通过名为selector
的函数来比对变更决定自身是否要更新。
如:
1 | const Foo = () => { |
我们再来看看另一派的实现: mobx
定义了一个 class
作为存储数据的store
, 而对于数据的任何修改都是用一种类似原生的方式 —— 直接赋值来实现的。即既可以直接访问store
中修改里面的值也可以通过调用store
暴露出的方法来修改数据。而数据的取值也是直接通过最简单的数据访问来实现的。
看上去非常美好,但是这是通过一些”黑魔法”来实现的,当执行makeAutoObservable(this)
的那一刻,原来的成员变量已经不是原来的数据了,已经变成了由mobx
包裹了一层实现的 可观察对象, 即这些对象的赋值与取值都不是原来的含义了。这也就是为什么mobx
可以实现reactive
响应式的原因。
这时候我们再来看看zustand
是怎么做的:
1 | import create from 'zustand' |
是的,只需要简单的一个对象就定义了一个store
,不需要特意去区分是state
还是action
, 也不需要特意去构造一个 class
来做的非常臃肿。zustand
就是用最简单的设计去做一些事情,甚至其核心代码只有500
行不到。
redux本身最核心的代码只有200行左右,但是如果要在react中使用需要加上
redux-react
和@reduxjs/toolkit
就远远超过了
另外可以注意到的是,zustand
天生设计了一种场景就是react
环境。其他”有野心”的状态管理库往往是从 vanilla
环境(纯js环境)开始设计,然后增加了对react
的支持,可能后续还会增加其他框架的支持。但是zustand
则不是,天生支持了react
环境,然后基于react
环境再衍生出vanilla
环境的支持。
那么很多人就会好奇,既然都支持vanilla
和react
,那么从哪个环境开始设计有什么区别么?
答案是有的,从不同的环境开始会从底层设计上就带来很大的偏差,最后落地到使用方来说就是基本使用需要调用的代码、运行时以及复杂度的差异。在我过去的开发经验告诉我这样是正确的,我几乎没有看见过哪个库能同时在多个框架中都能如鱼得水的。不同的框架会有不同的生态,而哪些特有的生态则是最贴合的,如redux
之于react
,pinia
之于vue
, rxjs
之于Angular
。很少有哪个库能够在多个环境中”讨好”的。因此zustand
就一种非常聪明的做法,专注于一点非常重要。
那么回到zustand
的基本使用,我们可以看到zustand
通过create
导出的是一个 react hook
, 通过这个hook
我们可以直接拿到store里面的state
和action
,非常类似于redux
的useSelector
。不同的是不需要dispatch
来推送action
, 也没有任何模板代码,数据类型天生区分了state
和action
, 只需要最简单的调用即可。
相比于mobx
, 也没有什么”黑魔法”, 简单而不容易出错。而且也不像mobx
会因为依赖class
实现的store
而引入天然的问题(比如作为数据store不应该有生命周期,而class
的constructor
天生就成为了生命周期的一种)
人的恐惧往往来自未知,
mobx
的对象就是这样的一个黑盒。这就是我不怎么喜欢mobx
的原因
那么,怎么应用到所有场景呢
zustand
是一种非常简单的实现,简单到让人觉得是不是总有一些场合是无法覆盖到的。而这就是我觉得zustand
是一件艺术品的原因。因为他总有巧妙的方式来不失优雅的适配任何我想要的场景。
在纯js中调用? 可以
1 | useBearStore.getState() |
通过getState
方式就可以获取最新的状态,在使用的过程中需要注意这是一个函数,目的是在运行时中获取到最新的值。里面的数据不是reactive
的。
想要有作用域? 可以
1 | import { createContext, useContext } from 'react' |
与原生 react
的 Context
结合就能实现作用域的效果。并且进一步将store
的创建合并到组件中,也能获得组件相关的生命周期。
想要在不同的store中相互调用?可以
通过useBearStore.getState()
就能实现store
间相互调用。当然需要注意管理好store
间的依赖关系。
想要中间件? 没问题
zustand
的库自带了一些中间件,其实现也非常简单。参考zustand/middleware
的实现可以学习如何制作zustand
的中间件。
想要处理异步action?没问题
在redux
早期,想要做异步action
是非常头疼的事情,而rtk
出来后会稍微好一点,但是也很麻烦。而在zustand
,可以非常简单
1 | const useFishStore = create((set) => ({ |
不足与思考
再好的设计如果不加限制也会出现 shit code
。想要把 zustand
这样小巧而精美的库用好而不是用坏需要一定的技术管理能力。盲目的去使用新的技术并不一定能给技术团队带来一些收益,但是可以带来新的思考。
另一方面,zustand
是一种全局store
的设计,不能说这种设计不好,但是也意味着带来了一种比较经典的技术难题,即依赖管理。当项目中出现相互依赖的时候,如何管理,怎么确保在后续的维护中不构成污染,在调试时不会引入噪音。这是我认为所有的全局store
都会面临的问题。