zustand是一个非常新潮的状态管理库,也是2021Star增长最快的React状态管理库。它的概念非常实用,API设计非常优雅,值得学习。概述首先介绍如何使用zustand。创建store通过create函数创建store,回调可以getgetset,类似于Redux的getState和setState,可以获取store的瞬时值,修改store。返回一个钩子来访问React组件中的商店。importcreatefrom'zustand'constuseStore=create((set,get)=>({bears:0,increasePopulation:()=>set(state=>({bears:state.bears+1})),removeAllBears:()=>set({bears:0})}))上面的例子是世界上唯一的商店。也可以通过createContext创建多实例store,结合Provider使用:>create(...)constApp=()=>(...)accessstore通过useStore访问组件中的store。与redux不同的是,store中既可以存储普通数据,也可以存储函数,还可以通过selector语法获取函数。因为函数引用是不可变的,下面的第二个例子实际上不会触发重新渲染:functionBearCounter(){constbears=useStore(state=>state.bears)return{bears}aroundhere...
}functionControls(){constincreasePopulation=useStore(state=>state.increasePopulation)returnoneup}如果多次调用useStore访问变量比较麻烦,你可以定义返回一个对象的compare函数:const{nuts,honey}=useStore(state=>({nuts:state.nuts,honey:state.honey}),shallow)细粒度的memo甚至可以跳过普通的compareby使用useCallback,并且只关心外部id值的变化,如:constfruit=useStore(useCallback(state=>state.fruits[id],[id]))原理是当id变化时,useCallback的返回值会变化,如果没有变化,useCallback的返回值也会变化,u的compare函数参考比较seStore会为真,这很聪明。setmergeandoverrideset函数的第二个参数默认为false,即合并值而不是覆盖整个store,所以你可以使用这个特性来清除store:constuseStore=create(set=>({salmon:1,tuna:2,deleteEverything:()=>set({},true),//清空整个store,包括动作}))asynchronous所有函数都支持异步,因为修改store不依赖于returnvalue,但是调用了set,??所以是否是异步数据流框架也是如此。监听指定的变量还是英文比较有表现力的,就是subscribeWithSelector。这个中间件允许我们在订阅函数上使用选择器。相对于redux传统的subscribe,我们可以有针对性的进行监控:})))//监听选定的变化,在这种情况下,当“爪子”发生变化时constunsub2=useStore.subscribe(state=>state.paw,console.log)//Subscribe也暴露之前的值constunsub3=useStore.subscribe(state=>state.paw,(paw,previousPaw)=>console.log(paw,previousPaw))//订阅也支持一个可选的相等函数constunsub4=useStore.subscribe(state=>[state.paw,state.fur],console.log,{equalityFn:shallow})//订阅并立即触发constunsub5=useStore.subscribe(state=>state.paw,console.log,{fireImmediately:true})还有一些组合中间件,immer,localstorage,reduxlike,devtools,combimestore后面就不细说了,都是一些细节场景.值得一提的是所有特征都是正交的。精读其实大部分使用的特性都是使用React语法,所以可以说50%的特性属于React的通用特性,只是写在了zustand文档中,貌似是React的特性zustand,所以这个库真的很擅长利用.创建存储实例任何数据流管理工具都有一个核心存储实例。对于zustand,它是vanilla.ts文件中定义的createStore。createStore返回一个类似于reduxstore的数据管理实例,有四个非常常用的API:,getState:constgetState:GetState=()=>state的实现就是这么简单粗暴。再看state,就是一个普通的对象:letstate:TState这是数据流最简单的一面,没有什么魔法,数据存储用的是一个普通的对象,仅此而已。再看setState,它做了两件事,修改状态和执行监听器:constsetState:SetState=(partial,replace)=>{constnextState=typeofpartial==='function'?部分(状态):部分如果(nextState!==状态){constpreviousState=statestate=replace?(nextStateasTState):Object.assign({},state,nextState)listeners.forEach((listener)=>listener(state,previousState))}}修改state也很简单,唯一重要的是listener(state,previousState),那么这些监听器是什么时候注册和声明的呢?其实listeners是一个Set对象:constlisteners:Set>=newSet()当注册和销毁的时机分别调用subscribe和destroy函数时,这个实现非常简单高效。相应的代码就不贴了。显然,subscribe时注册的listener函数会作为listener加入到listeners队列中,并在setState发生时被调用。最后我们看一下createStore的定义和结束:functioncreateStore(createState){letstate:TStateconstsetState=/**...*/constgetState=/**...*//**...*/constapi={setState,getState,subscribe,destroy}state=createState(setState,getState,api)returnapi}虽然这个state是一个简单的对象,但是回头看文档,我们可以在create和use中创建一个store给状态赋值的回调,即set、get、api从上面代码的倒数第二行导入:import{create}from'zustand'constuseStore=create((set,get)=>({bears:0,increasePopulation:()=>set(state=>({bears:state.bears+1})),removeAllBears:()=>set({bears:0})}))此时,所有初始化store的API的来龙去脉都整理好了,逻辑简单明了。create函数的实现上面我们解释了如何创建store实例,但是这个实例是底层API。文档中介绍的create函数定义在react.ts文件中,调用createStore创建一个框架无关的数据流。之所以在react.ts中定义create,是因为返回的useStore是一个Hooks,所以具有React环境的特点,因此得名。这个函数第一行调用createStore创建基础store,因为是框架内部的API,所以名字也叫api:constapi:CustomStoreApi=typeofcreateState==='function'?createStore(createState):createStateconstuseStore:any=(selector:StateSelector=api.getStateasany,equalityFn:EqualityChecker=Object.is)=>/**...*/下面所有的代码都在创建useStore这个函数,我们看一下它的内部实现:简单来说就是用subscribe监听变化,需要的时候强制刷新当前组件,把最新的状态传给useStore。所以第一步当然是创建forceUpdate函数:const[,forceUpdate]=useReducer((c)=>c+1,0)as[never,()=>void]然后调用API并传递给selector,并调用equalityFn(此函数可自定义)判断状态是否发生变化:conststate=api.getState()newStateSlice=selector(state)hasNewStateSlice=!equalityFn(currentSliceRef.currentasStateSlice,newStateSlice)如果状态改变了,就更新currentSliceRef.current:useIsomorphicLayoutEffect(()=>{if(hasNewStateSlice){currentSliceRef.current=newStateSliceasStateSlice}stateRef.current=stateselectorRef.current=selectorequalityFnRef.current=equalityFnerroredRef.current=false})useIsomorphicLayoutEffect是同构框架中常用的API例程是前端环境的useLayoutEffect和节点环境的useEffect:讲解currentSliceRef和newStateSlice的作用。我们看一下useStore的最终返回值:constsliceToReturn=hasNewStateSlice?(newStateSliceasStateSlice):currentSliceRef.currentuseDebugValue(sliceToReturn)returnsliceToReturn发现逻辑是这样的:如果状态发生变化,则返回新状态,否则返回旧状态,这样可以保证比较函数判断相等时,返回对象的引用完全相同。这是不可变数据的核心实现。此外,我们还可以学习阅读源码的技巧,即经常跳读。Sohowdoyouupdatethestorewhentheselectorchanges?中间还有一段核心代码,调用subscribe,相信大家已经猜到了,下面是核心代码片段:useIsomorphicLayoutEffect(()=>{constlistener=()=>{try{constnextState=api.getState()constnextStateSlice=selectorRef.current(nextState)if(!equalityFnRef.current(currentSliceRef.currentasStateSlice,nextStateSlice)){stateRef.current=nextStatecurrentSliceRef.current=nextStateSliceforceUpdate()}}捕获(错误){errorredRef.currentd=()}}constunsubscribe=api.subscribe(listener)if(api.getState()!==stateBeforeSubscriptionRef.current){listener()//订阅前状态已经改变}returnunsubscribe},[])这段代码应该先看api.subscribe(listener),它使得任何setState触发监听器的执行,监听器使用api.getState()获取最新状态,获取最后一个比较函数equalityFnRef来执行判断值是否发生变化,如果发生变化,更新currentSliceRef并执行强制刷新(调用forceUpdate)。context的实现注意上下文语法,可以创建多个互不干扰的store实例:importcreatefrom'zustand'importcreateContextfrom'zustand/context'const{Provider,useStore}=createContext()constcreateStore=()=>create(...)constApp=()=>(...)首先我们知道create创建的store是不会互相干扰的。问题是create返回的只有一个useStore的实例,并且没有声明作用域,那么如何构造上面的API呢?首先Provider存储create返回的useStore:conststoreRef=useRef()storeRef.current=createStore()然后useStore本身并没有真正实现数据流功能,而是获取并返回提供的storeRef:constuseStore:UseContextStore=(selector?:StateSelector,equalityFn=Object.is)=>{constuseProviderStore=useContext(ZustandContext)返回useProviderStore(选择器作为StateSelector,equalityFn)}所以核心逻辑还是在create函数中,context.ts只是使用ReactContext来“注入”useStore对组件来说,利用ReactContext特性,这个注入可以有多个实例,并且不会互相影响。不需要实现中间件。比如看这个redux中间件的例子:import{redux}from'zustand/middleware'constuseStore=create(redux(reducer,initialState))可以把zustand的用法改成reducer,其实就是用到了函数式的概念,reduxfunction可以自己getset,get,api。如果你想保持API不变,你可以直接返回回调。如果你想改变用法,你可以返回一个特定的结构。就这么简单。为了加深理解,我们来看一下redux中间件的源码:exportconstredux=(reducer,initial)=>(set,get,api)=>{api.dispatch=action=>{set(state=>reducer(state,action),false,action)returnaction}api.dispatchFromDevtools=truereturn{dispatch:(...a)=>api.dispatch(...a),...initial}}封装集合,get,api对于redux的API来说:dispatch的本质就是调用set。综上所述,zustand是一款精致的React数据流管理工具。其自身的框架无关分层合理,中间件实现巧妙。值得学习。讨论地址为:Jingdu《zustand 源码》·Issue#392·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)