前端状态管理库对比

状态管理

为什么jQuery时代,我们并没有谈论状态管理,但是在React,Vue的时代,我们日常开发,基本离不开一些状态管理库?

什么是状态?

目前比较常见的状态管理库有Redux、Mobx、Rematch、Zustand、Recoil、Jotai、Valtio等。其中Redux、Mobx、Rematch、Zustand、Valtio不依赖UI框架,在其他框架中也能使用。本文只对不依赖UI框架的状态管理库进行介绍,希望能对你在不同的UI框架中进行状态管理库选型时提供帮助。首先看下这几个框架近几年的npm下载趋势,可以看到redux仍遥遥领先~


本文将对Redux、Mobx、Rematch、Zustand、Valtio几个库进行介绍,从背景-核心工作流-适用场景-使用-原理进行介绍

对这些部分不感兴趣的同学可以直接跳到最后一节对比(省流篇),以表格的形式清晰、简洁的展示各个框架的区别,方便你的选型~

Redux简介

Redux是什么?

Redux的工作流程:

首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。

然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State

State一旦有变化,Store就会调用监听函数,来更新View。

如下图:

为什么要使用Redux?

前端涉及到大量复杂的逻辑判断、交互和异步操作。当应用规模越来越大时,与前端界面相关联的状态也越来越多,界面的改变就会变得难以预测。比如,当视图中的某个标签A发生了改变,在传统的开发方式中,我们需要去找到代码里所有修改标签A的地方。但是假如我们使用Redux去统一管理状态,标签A受控于状态A,而状态A的改变只会通过派发一个action来改变。这样标签A的状态就变得易于控制,且容易预测。

Redux有哪些使用场景?

应用中有很多state在多个组件中需要使用

应用state会随着时间的推移而频繁更新

更新state的逻辑很复杂

中型和大型代码量的应用,很多人协同开发

Redux的使用

Redux是一个小型的独立JS库,不依赖于任意框架(库),只要subscribe相应框架(库)的内部方法,就可以使用该应用框架保证数据流动的一致性。在react中使用时,可以使用React-Redux库,它是React官方的ReduxUI绑定库

在React框架中使用

首先看一张react-redux和redux的关系图:

总结一下React-Redux做了哪些事情(简化版,详细版参见文档):

包装渲染函数

对外只提供了一个Provider和connect的方法,隐藏了关于store操作的很多细节

Provider接受store作为参数,并且通过context把store传给所有的子组件

子组件通过connect包裹了一层高阶组件,高阶组件会通过context结合mapStateToProps和store,把数据传给被包裹的组件

避免没有必要的渲染

缓存上次的计算的props,然后用新的props和旧的props进行对比,如果两者相同,就不调用rer

更新机制

本质还是使用redux的进行更新订阅,进行更新派发

connect包裹组件时,会通过react-redux内部的subscription去调用redux的注册监听

任意一个组件dispatch了一次,所有组件的更新函数都要被batch执行,所有connect包裹的组件都会去判断是否真正需要执行更新

使用示例:

单独使用Redux

使用createStore创建全局store:createStore返回一个对象,包含dispatch、subscribe、getState、replaceReducer等方法

(action):派发action,组合action和当前state,获取到最新state数据,遍历收集的订阅函数

(listener):订阅监听函数,存放在数组中,(action)时遍历执行。

():获取当前store的数据

举个在小程序中使用的例子:

js复制代码//省略全局,同上importStorefrom'../../store'import{changeUserName}from'../../store/actions/user'Page({data:{userInfo:{}},onLoad(){constfn=()={const{userReducer}=()({userInfo:userReducer})}fn()(()={fn()})},getUserProfile(){({success:(res)={(changeUserName())},fail:(error)={("getUserInfoerror",error)},})}})//wxmlviewclass="app"blockwx:if="{{!}}"viewbindtap="getUserProfile"获取头像昵称/view/blockblockwx:elseimageclass="userinfo-avatar"src="{{}}"/imagetextclass="userinfo-nickname"{{}}/texttextclass="userinfo-ger"{{?'女':'男'}}/text/block/view//{CHANGE_NAME}from"./type"exportconstchangeUserName=newName={return{type:CHANGE_NAME,payload:newName}}//省略,同上
Redux实现主流程1.createStore

createStore主要用于Store的生成,返回一个store对象包含dispatch、subscribe、getState、replaceReducer等方法。dispatch了一个initAction,为了生成初始的State树

getState:返回当前的状态

replaceReducer:替换当前的Reducer并重新初始化了State树

dispatch:分发action,修改State的唯一方式

subscribe:入参函数放入监听队列,返回取消订阅函数

getState和replaceReducer函数比较简单,不再展开

2.

调用Reducer,传参(currentState,action)

按顺序执行listener

返回action

3.
createStore`函数中存储`listeners`设计了两个数组:`nextListeners、currentListeners

每次dispatch,都从currentListeners中取订阅函数

每次subscribe,都往nextListeners中增加订阅函数

这样设计的目的是为了满足redux的设计理念:无论是新增订阅或是取消订阅,都不会在当前dispatch阶段生效,只会在下次dispatch阶段生效

辅助功能

仅介绍使用的比较多的函数:

与中间件实现相关的compose,applyMiddleWare

combineReducers

combineReducers

作用:合并多个reducer为一个函数combination

使用场景:应用比较大的时候,将Reducer按照模块拆分

中间件

先通过一张图看一下,什么是中间件

诞生背景:

Reduxreducer设计理念之一是“绝对不能包含副作用”副作用:除函数返回值之外的任何变更,包括state的更改或者其他行为

一些常见的副作用有:

在控制台打印日志

异步更新state

修改存在于函数之外的某些state,或改变函数的参数

生成随机数或唯一随机ID

为了使redux能支持这些副作用,设计了middleware,用以支持增加一些副作用逻辑代码中间件的本质:中间件就是一个函数,对方法进行了改造,在发出Action和执行Reducer这两步之间,添加了其他功能

常用的中间件有:

redux-thunk

redux-logger

redux-saga

先看下中间件如何在代码里使用

从constStore=createStore(rootReducer,applyMiddleware(thunk))看,中间件是如何实现的?从createStore里看,上面的代码相当于执行applyMiddleware(…middlewares)(createStore)(reducer,preloadedState)

先粗略的看下applyMiddleware,最终的dispatch是通过compose函数组装的,首先看一下compose是怎么组装函数的

compose
compose`这个方法,主要用来组合传入的一系列函数`compose(f,g,h)`等价于`return(args)=f(g(h(args)))js复制代码exportdefaultfunctioncompose(funcs){if(===0){return(arg)=arg}if(===1){returnfuncs[0]}​((a,b)=(args)=a(b(args)))}
applyMiddleware

最终applyMiddleware的执行结果是返回了一个正常的Store和一个被变更过的dispatch方法,实现了对Store的增强。

js复制代码functionapplyMiddleware(middlewares){returncreateStore=(reducer,preloadedState)={conststore=createStore(reducer,preloadedState)letdispatch=()={thrownewError('Dispatchingwhileconstructingyourmiddlewareisnotallowed.'+'Othermiddlewarewouldnotbeappliedtothisdispatch.')}constmiddlewareAPI={getState:,dispatch:(action,args)=dispatch(action,args)}constchain=(middleware=middleware(middlewareAPI))//假设chain是[f,g,h],最终生成的dispatch相当于f(g(h()))//相当于把原有的dispatch层层过滤,变成新的dispatchdispatch=compose(chain)()return{store,dispatch}}}

结合applyMiddleware的实现,我们在写中间件时要注意:

中间件是要多个首尾相连的,需要一层层的“加工”,所以要有个next方法来独立一层确保串联执行

dispatch增强后也是个dispatch方法,也要接收action参数,所以最后一层肯定是action

中间件内部需要用到Store的方法,所以Store放到顶层

现在再来看看上面的中间件使用的例子是如何工作的假设我们现在有三个中间件,f,g,hdispatch=f(g(h()))中间件一般是【自己实现一个中间件】一节中的格式的函数,这样的话整个执行链路就是这样的:

h(),此时,next=,返回一个【action={}】这样的函数

g(h()),next=h(),返回一个【action={}】这样的函数

f(g(h())),next=g(h()),返回一个【action={}】这样的函数

当业务代码中调用
dispatch
的时候

如果派发的action不是一个函数,执行next(action),会依次执行g(h())-h()-(action)

如果派发的action是一个函数,执行action(dispacth,getState),全局的,和store的dispatch作为参数传递给业务侧定义的action函数,如changeUserNameAsync,最终执行dispatch(action),等价于使用派发action

自己实现一个中间件
js复制代码//可以实现异步actionconstmyMiddleWare=({getState,dispatch})=next=action={if(typeofaction==='function'){('函数Action',action)returnaction(dispacth,getState)}('objectaction',action)returnnext(action)}
Mobx简介

Mobx是什么?

Mobx也是一个状态管理工具,和Redux类似,不依赖于前端UI框架库。Mobx的作者觉得Redux比较繁琐,采用响应式编程的方式设计了Mobx。

Mobx核心概念:

State(状态):驱动应用程序的数据

Actions(动作):去改变state的代码

Derivations(派生):任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生,比如:用户界面,后端集成,衍生数据。派生分为两种:

Computedvalues:使用纯函数从当前state中衍生出的值

Reactions:当State改变时需要自动运行的副作用(命令式编程和响应式编程之间的桥梁)

Mobx的工作流程

事件触发了Actions,Reactions可以经过事件调用Actions

Actions作为唯一修改State的方式,修改了State

State的修改更新了计算值Computed

计算值的改变引起了Reactions的改变

Mobx原则:

所有的derivations将在state改变时自动且原子化地更新。因此不可能观察中间值。

所有的derivations默认将会同步更新,这意味着action可以在state改变之后安全的直接获得computed值。

computedvalue的更新是惰性的,任何computedvalue在需要他们的副作用发生之前都是不激活的。

所有的computedvalue都应是纯函数,他们不应该修改state。

Mobx适用于哪些场景?

首先Mobx和Redux一样都是状态管理库,所以Redux的使用场景同样适用于mobx

mobx比较容易上手,适用于需要快速迭代的小项目

如果你之前已经习惯开发vue,因为mobx和vue都是响应式编程,所以使用mobx会更顺手一些

Mobx的使用单独使用mobx

定义一个可观察的状态
js复制代码constuserStore=observable({
userInfo:{
nickName:'xxx',
ger:'',
age:0
}
})

使用autorun定义响应函数。autorun函数接受一个函数作为参数,每当该函数所观察的值发生变化时,它都会运行。
js复制代码autorun(()={
constnameBox=('nameClick')
('click',()={
()
})

computed:computed属性有两个特性。使用属性.get()获取其值

被缓存:每当读取computed值时,如果其依赖的状态或其他computed值未发生变化,则使用上次的缓存结果,以减少计算开销,对于复杂的computed值,缓存可以大大提高性能

惰性计算:只有computed值被使用时才重新计算值。反言之,即使computed值依赖的状态发生了变化,但是它暂时没有被使用,那么它不会重新计算。

js复制代码constuserStore=observable({
age:computed(function(){
(()*200)
})
})
autorun(()={
('',())
})
//因为age没有依赖状态,所以这里其实每次打印都会是同一个值

在React框架中使用

在React框架中使用时,可以借助mobx-react或mobx-react-lite(轻量级)框架

mobx-react相对于mobx,主要增加了以下几点:

提供了provider和inject,方便管理多store

提供了一个observer方法,它是一个高阶组件,它接收React组件并返回一个新的React组件,返回的新组件能响应(通过observable定义的)状态的变化

使用方式:

定义store通过provider注入:多store的话,可以聚合成一个store
js复制代码importUserStorefrom"./userStore"
importCountStorefrom"./countStore"

exportdefault{
userStore:newUserStore(),
countStore:newCountStore()
}

在根组件中通过Provider注入store
js复制代码importReactfrom'react'
import{Provider}from'mobx-react'
importUserCardMobxfrom'./Components/UserCardMobx';
importstorefrom'./store/mobx/index'
functionApp(){
return(
Provider{store}
UserCardMobx/
/Provider
);
}

exportdefaultApp;

在React组件中使用Store:利用observer,让React组件响应store的变化

Mobx实现

MobX的主要设计思想:「函数响应式编程」+「可变状态模型」

实现:mutable+proxy(为了兼容性,proxy实际上使用实现)

简单总结:

用或者Proxy来拦截observable包装的对象属性的get/set

在autorun或者reaction执行的时候,会触发依赖状态的get,此时将autorun里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。

当修改状态的时候会触发set,此时会通知前面关联的函数,重新执行他们

mobx的源码相比于redux真的很晦涩

只介绍依赖收集observable和响应更新autorun,其他不再介绍

observable

mobx有多种定义观测对象的方式,有makeAutoObservable,observable,makeObservable,但无论是使用哪个API,最终都会根据观测的数据类型,如array,object等调用不同的拦截方法实现观测

makeAutoObservable,observable,makeObservable的区别:

使用上的区别:

makeObservable捕获已经存在的对象属性并且使得它们可观察

makeAutoObservable默认情况下它将推断所有的属性

observable:复制传入的对象,并将传入对象的所有属性都变成可观察的

底层数据基类的区别:

make(Auto)Observable是基于ComputedValue类

observable:支持使用proxy的环境直接使用proxy拦截整个对象。不支持使用proxy的环境,使用ObservableValue类

操作对象的区别:

make(Auto)Observable会修改第一个参数传入的对象

observable会创建一个可观察的副本对象。

使用建议:

如果你想把一个对象转化为可观察对象,而这个对象具有一个常规结构,其中所有的成员都是事先已知的,建议使用makeObservable,因为非代理对象的速度稍快一些,而且它们在调试器和中更容易检查

ComputedValue和ObservableValue的区别:

ComputedValue既是观察者,也是被观察者,它有自己的依赖,同时又是别人的依赖,所以ComputedValue中既有ObservableValue的特性,又有Reaction的特性。

computed装饰器是用来装饰get方法的,它将这个方法包装为ComputedValue,在进行get操作的时候,会判断依赖项是否发生变化,进行方法的执行,判断值是否发生变化,通知观察者自身发生了变化。

某个ObservableValue值发生改变后,会调用观察者的onBecomeStale方法,表明这个观察者的依赖发生变更,如果这个观察者是ComputedValue,那么这个观察者的值并没有发生改变,而是当调用到ComputedValue的时候,才重新计算这个值。

computed和autorun进行依赖收集的方式一模一样,都是借助全局变量完成的。

对于object数据类型而言,核心是调用asObservableObject,该函数的调用流程是:asObservableObject-ObservableObjectAdministration

make(Auto)Observable:_-_

observable:_-defineObservableProperty_

observable核心:

observable的目的是将正常的对象变成ObservableValue,adm是管理ObservableValue的,decorator和enhance用于配置adm。将每个属性都要转变为ObservableValue后,我们对属性的set和get其实都是对adm的write和read。

js复制代码defineObservableProperty_(key:PropertyKey,value:any,enhancer:IEnhancerany,proxyTrap:boolean=false):boolean|null{try{startBatch()//省略一些非核心逻辑constcachedDescriptor=getCachedObservablePropDescriptor(key)constdescriptor={configurable:?_:true,enumerable:true,get:,set:}//Defineif(proxyTrap){if(!(_,key,descriptor)){returnfalse}}else{//target对象的key的descriptor在这里被设置完成了//将该属性的get和set方法代理到adm的get和set方法上,然后使用defineProperty将这个属性赋给之前建立的空对象。defineProperty(_,key,descriptor)}//生成ObservableValueconstobservable=newObservableValue(value,enhancer,**DEV**?`${_}.${()}`:"",false)//将对象的属性设置成ObservableValue,放到values中统一管理。统一的发布-订阅_.set(key,observable)//Notify(valuepossiblychangedbyObservableValue)_(key,_)}finally{Batch()}returntrue}
autorun

autorun工作流程如图:

autorun核心流程:

创建一个Reaction实例

将响应函数view先包裹一层track函数,并绑定到Reaction内部的onInvalidate_

通过
_()
调度执行

_()会先将这个Reaction实例放入

执行runReactions-runReactionsHelper

在runReactionsHelper里面会遍历我们的pingReactions数组,执行里面的reaction实例的runReaction_方法

执行runReaction_时,就会执行onInvalidate_,也就是track(view)

js复制代码exportfunctionautorun(view:(r:IReactionPublic)=any,opts:IAutorunOptions=EMPTY_OBJECT):IReactionDisposer{//省略环境判断逻辑constname:string=opts?.name??(**DEV**?(viewasany).name||"Autorun@"+getNextId():"Autorun")construnSync=!!:Reactionif(runSync){//normalautorunreaction=newReaction(name,function(this:Reaction){(reactionRunner)},,)}else{constscheduler=createSchedulerFromOptions(opts)//debouncedautorunletisScheduled=falsereaction=newReaction(name,()={if(!isScheduled){isScheduled=truescheduler(()={isScheduled=falseif(!_){(reactionRunner)}})}},,)}functionreactionRunner(){view(reaction)}if(!opts?.signal?.aborted){_()}_(opts?.signal)}

下面详细介绍下track(view)的执行流程,看下track函数

js复制代码track(fn:()=void){//省略startBatch()constnotify=isSpyEnabled()_=trueconstprevReaction=//=this//fn即为刚刚绑定的view函数constresult=trackDerivedFunction(this,fn,undefined)=_=_=falseif(_){clearObserving(this)}if(isCaughtException(result))_()if(**DEV**notify){spyReport({time:()-startTime})}Batch()}

可以看到,trackDerivedFunction会调用view函数。在执行view函数的时候,如果里面依赖了被observable包裹对象的属性,那么就会触发属性的get方法

可以看到,当触发属性的get方法时,会执行reportObserved,会将observable挂载到_上面

再次回到trackDerivedFunction函数,函数接着往下执行到bindDepencies函数,将Reaction实例和observable关联起来。bindDepencies不再详细展开

js复制代码functiontrackDerivedFunctionT(derivation:IDerivation,f:()=T,context:any){//省略if(===true){//这里触发了,继而执行了reportObserved//_[_++]=observer;result=(context)}else{try{result=(context)}catch(e){result=newCaughtException(e)}}=prevTracking//执行bindDepencies函数,将Reaction实例和observable关联起bindDepencies(derivation)warnAboutDerivationWithoutDepencies(derivation)allowStateReads(prevAllowStateReads)returnresult}
Zustand简介

Zustand是什么?

Zustand的工作流程:

通过create方法去创建一个Store,并在Store里定义我们需要维护的状态和改变状态的方法

create函数实际上返回了一个hook,通过调用这个hook,可以在组件中去订阅某个状态,或者获取改变某个状态的方法

Zustand的特点:

不需要使用contextprovider包裹你的应用程序

可以做到瞬时更新(不引起组件渲染完成更新过程)

不依赖react上下文,引用更加灵活

当状态发生变化时重新渲染的组件更少

集中的、基于操作的状态管理

基于不可变状态进行更新,store更新操作相对更加可控

Zustand的使用单独使用

不在react或vue框架中使用的话,要使用zustand/vanilla库

以下使用示例为在小程序中的使用例子

创建store

js复制代码import{createStore}from'zustand/vanilla'constdateStore=createStore((set)=({date:{current:newDate().toLocaleDateString()},changeTime:()=set((state)=({date:{,current:newDate().toLocaleString()}})),changeTimeDelay:async()={awaitnewPromise(resolve={setTimeout(()={resolve()},10000)})set((state)=({date:{,current:newDate().toLocaleString()}}))}}))constcouponStore=createStore((set)=({coupon:{name:''},changeName:()=set((state)=({date:{,name:`${newDate().getTime()}`}})),}))export{dateStore,couponStore}

在view中使用

js复制代码import{dateStore,couponStore}from"../../store/zustand"Page({data:{date:{},coupon:{}},onLoad(){const{date={}}=()const{coupon={}}=()({date,coupon})(state={({date:})})(state={({coupon:})})},changeTime(){().changeTime()},changeTimeDelay(){().changeTimeDelay()},changeCouponName(){().changeName()}})
在React框架中使用

创建store:创建的store是一个hook,你可以放任何东西到里面(基础变量,对象、函数),状态必须不可改变地更新,set函数合并状态以实现状态更新

store绑定组件:可以在任何地方使用钩子,不需要提供provider,基于selector获取业务state,组件将在状态更改时重新渲染

使用中间件

zustand官方提供了六个中间件:

immer:给set加入immer的功能

persist:用于持久化存储状态,存储到例如localStorage、IndexedDB等,当应用重新加载时可以从存储引擎中恢复状态。

redux:利用redux的dispatch\reducer方式编写,通过这个中间件可以很方便的把useReducer或者redux管理的状态迁移到zustand中

combine:合并state

devtools:利用开发者工具调试/追踪Store

subscribeWithSelector:让我们把selector用在subscribe函数上

js复制代码import{immer}from'zustand/middleware/immer'constuseDateStore=create(immer(()))
Zustand的实现createStore

暴露五个api:

setState:修改state,遍历订阅列表,执行订阅函数

getState:获取当前state

getInitialState:获取初始化state

subscribe:添加订阅函数

destroy:清空全部订阅函数

js复制代码constcreateStoreImpl=(createState)={letstate//创建一个Set结构来维护订阅者。constlisteners=newSet()//定义更新数据的方法,partial参数支持对象和函数,replace指的是全量替换store还是merge//如果是partial对象时,则直接赋值,否则将上一次的数据作为参数执行该方法。//然后利用进行新老数据的浅比较,如果前后发生了改变,则进行替换//并且遍历订阅者,逐一进行更新。constsetState=(partial,replace)={constnextState=typeofpartial==='function'?partial(state):partialif(!(nextState,state)){constpreviousState=statestate=replace??(typeofnextState!=='object'||nextState===null)?nextState:({},state,nextState)((listener)=listener(state,previousState))}}//getState方法返回当前Store里的最新数据constgetState=()=stateconstgetInitialState=()=initialState//添加订阅方法,并且返回一个取消订阅的方法constsubscribe=(listener)={(listener)//Unsubscribereturn()=(listener)}//清空全部订阅函数constdestroy=()={//省略环境判断()}constapi={setState,getState,getInitialState,subscribe,destroy}constinitialState=(state=createState(setState,getState,api))returnapi}
create

先调用上文createStore方法生成store

再利用useSyncExternalStoreWithSelector方法对react进行集成

js复制代码//对React进行集成exportfunctionuseStore(api,selector,equalityFn){//利用useSyncExternalStoreWithSelector,对store里的所有数据进行选择性的分片constslice=useSyncExternalStoreWithSelector(,,||,selector,equalityFn)useDebugValue(slice)returnslice}
useSyncExternalStoreWithSelector
useSyncExternalStoreWithSelector`是[`useSyncExternalStore`]()指定选择器优化版,`useSyncExternalStore`是`react18`新增的特性,所以`react`团队发布了这个向后兼容的包`use-sync-external-store/shim`,以便`18`以前的版本也可以使用(当然要大于`16.8`版本),主要功能是订阅外部`store`的`hook

useSyncExternalStoreWithSelector接收五个参数:

subscribe:外部store的订阅方法

getSnapshot:相当于getState

getServerSnapshot:返回服务端渲染期间使用的state

selector:返回指定状态的selector函数,比如state上有个data数据,只想得到data,就可以使用state=

equalFn:对比函数,决定是否更新

工作机制:

useSyncExternalStore是当store的状态改变时,订阅函数会执行,此时react会调用getSnapshot和之前的状态快照比较(通过比较),如果状态发生改变,组件会重新渲染

useSyncExternalStoreWithSelector

useSyncExternalStore
的基础上增加了
selector

isEqual
,可以减少
re-rer
的次数,也能缓存
state

在store不变的情况下,重复调用getSnapshot返回同一个值

js复制代码functionuseSyncExternalStoreWithSelector(subscribe,getSnapshot,getServerSnapshot,selector,isEqual,){constinstRef=useRef(null);letinst;if(===null){inst={hasValue:false,value:null,};=inst;}else{inst=;}/**-每次re-rer都会获得一个新的selector-所以getSelection在re-rer后都是新的,但是因为有以及isEqual-当isEqual的时候返回缓存的值,也就是getSelection的返回值不变-不会再次re-rer,减少了re-rer的次数*/const[getSelection,getServerSelection]=useMemo(()={lethasMemo=false;letmemoizedSnapshot;letmemoizedSelection;constmemoizedSelector=(nextSnapshot)={if(!hasMemo){//第一次调用hook时,没有缓存hasMemo=true;memoizedSnapshot=nextSnapshot;constnextSelection=selector(nextSnapshot);//需要用户自己提供isEqualif(isEqual!==undefined){if(){constcurrentSelection=;if(isEqual(currentSelection,nextSelection)){memoizedSelection=currentSelection;returncurrentSelection;}}}memoizedSelection=nextSelection;returnnextSelection;}constprevSnapshot=memoizedSnapshot;constprevSelection=memoizedSelection;if(is(prevSnapshot,nextSnapshot)){//快照与上次相同,重复使用之前的结果returnprevSelection;}//快照已更改,需要获取新的快照constnextSelection=selector(nextSnapshot);//如果提供了自定义isEqual函数,会使用它来检查数据是否,已经改变。//如果未改变,返回之前的结果,React会退出渲染if(isEqual!==undefinedisEqual(prevSelection,nextSelection)){returnprevSelection;}memoizedSnapshot=nextSnapshot;memoizedSelection=nextSelection;returnnextSelection;};constmaybeGetServerSnapshot=getServerSnapshot===undefined?null:getServerSnapshot;constgetSnapshotWithSelector=()=memoizedSelector(getSnapshot());constgetServerSnapshotWithSelector=maybeGetServerSnapshot===null?undefined:()=memoizedSelector(maybeGetServerSnapshot());return[getSnapshotWithSelector,getServerSnapshotWithSelector];},[getSnapshot,getServerSnapshot,selector,isEqual]);constvalue=useSyncExternalStore(subscribe,getSelection,getServerSelection,);useEffect(()={=true;=value;},[value]);useDebugValue(value);returnvalue;}
中间件

特点:

zustand的中间件实际上是一个高阶函数,它的入参和create函数相同,本质上是对create时传入的初始化config做了一层包裹,注入特定的逻辑

zustand核心源码中并没有发现任何和中间件有关的代码

和redux一样,本质还是利用函数组合

来看下redux中间件源码

js复制代码exportconstredux=(reducer,initial)=(set,get,api)={=action={set(state=reducer(state,action),false,action)returnaction}=truereturn{dispatch:(a)=(a),initial}}//使用:create(redux(reducer,initialState))//自定义一个中间件,记录状态更新constmiddleware1=(config)=(set,get,api)=config((args)={("applying",args);set(args);("newstate",get());},get,api)
Valtio简介

Valtio是什么?

Valtio是一个基于Proxy实现,更容易上手的状态管理工具

双向数据绑定

发布订阅模式

Valtio的工作流程:

使用proxy拦截对象,得到state

使用useSnapshot获取state,返回一个不可变的snapshot

操作proxystate获取新的snapshot,触发组件rerer

Valtio适用于哪些场景?

上手简单:使用体验和mobx基本一致

适用于需要简单的自动更新的场景

Valtio的特点

容易使用和理解:没有复杂的概念,只有两个核心方法
proxy

useSnapshot

proxy函数创建状态代理对象

useSnapshot获取状态快照

细粒度渲染:使用useSnapshot可以只在状态变化的部分触发组件的重新渲染

Valtio的使用单独使用Valtio

要点:

使用proxy拦截state

使用snapshot获取一个不可变对象

使用subscribe订阅state改变

举个在小程序中使用的例子

js复制代码import{proxy,snapshot,subscribe}from'valtio'constdateState=proxy({current:newDate().toLocaleString()})constchangeTime=()={=newDate().toLocaleString()}constchangeTimeDelay=async()={awaitnewPromise((resolve)={setTimeout(()={resolve()},10000)})changeTime()}Page({data:{date:{}},onLoad(){constfn=()={constdate=snapshot(dateState)({date})}fn()subscribe(dateState,()={fn()})},changeTime,changeTimeDelay})
在React框架中使用ValtioValtio的实现proxy

要点:

最终会调用函数proxyFunction

内部会用到两个关键的数据结构
proxyStateMap

proxyCache

proxyStateMap:跟踪和管理代理对象与原始对象之间的映射关系。在代理对象创建时建立映射,并在需要时提供从代理对象到原始对象或从原始对象到代理对象的查询功能

proxyCache:缓存已经创建的代理对象,以避免同一个原始对象被多次创建代理对象,目的是提高性能和避免不必要的内存消耗

js复制代码proxyFunction=(initialObject)={//省略校验//proxyCache用来存储已经创建的代理对象constfound=(initialObject)//如果cache中有直接返回if(found){returnfound}letversion=versionHolder[0]constlisteners=newSet()//通知更新constnotifyUpdate=(op,nextVersion=++versionHolder[0])={if(version!==nextVersion){version=((listener)=listener(op,nextVersion))}}letcheckVersion=versionHolder[1]//确保代理对象和其versionHolder的版本匹配,确保依赖关系在需要时得到正确的通知constensureVersion=(nextCheckVersion=++versionHolder[1])={if(checkVersion!==nextCheckVersion!){checkVersion=(([propProxyState])={constpropVersion=propProxyState[1](nextCheckVersion)if(propVersionversion){version=propVersion}})}returnversion}//创建属性监听constcreatePropListener=(prop)=(op,nextVersion)={constnewOp=[op]newOp[1]=[prop,(newOp[1])]notifyUpdate(newOp,nextVersion)}constpropProxyStates=newMap()//向代理对象添加属性监听器,用于监听某个属性的变化,并在变化时触发指定的回调函数constaddPropListener=(prop,propProxyState)={//省略环境判断代码if(){//有listener,设置removeconstremove=propProxyState[3](createPropListener(prop))(prop,[propProxyState,remove])}else{(prop,[propProxyState])}}//移除属性监听constremovePropListener=(prop)={constentry=(prop)if(entry){(prop)entry[1]?.()}}//返回移除监听函数constaddListener=(listener)={(listener)//当有listener时,遍历propProxyStates,加上removeif(===1){(([propProxyState,prevRemove],prop)={//省略环境判断代码constremove=propProxyState[3](createPropListener(prop))(prop,[propProxyState,remove])})}//移除监听constremoveListener=()={(listener)if(===0){(([propProxyState,remove],prop)={if(remove){remove()(prop,[propProxyState])}})}}returnremoveListener}//一个新对象constbaseObject=(initialObject)?[]:((initialObject))consthandler={//删除属性deleteProperty(target,prop){constprevValue=(target,prop)removePropListener(prop)constdeleted=(target,prop)if(deleted){notifyUpdate(['delete',[prop],prevValue])}returndeleted},//修改属性set(target,prop,value,receiver){consthasPrevValue=(target,prop)constprevValue=(target,prop,receiver)if(hasPrevValue(objectIs(prevValue,value)||((value)objectIs(prevValue,(value))))){returntrue}removePropListener(prop)if(isObject(value)){value=getUntracked(value)||value}letnextValue=value//处理异步if(valueinstanceofPromise){((v)={='fulfilled'=vnotifyUpdate(['resolve',[prop],v])}).catch((e)={='rejected'=enotifyUpdate(['reject',[prop],e])})}else{//新值是个可代理对象if(!(value)canProxy(value)){nextValue=proxyFunction(value)}constchildProxyState=!(nextValue)(nextValue)if(childProxyState){addPropListener(prop,childProxyState)}}(target,prop,nextValue,receiver)notifyUpdate(['set',[prop],value,prevValue])returntrue},}//newProxy-newProxy(target,handler)constproxyObject=newProxy(baseObject,handler)(initialObject,proxyObject)constproxyState=[baseObject,ensureVersion,createSnapshot,addListener,](proxyObject,proxyState)(initialObject).forEach((key)={constdesc=(initialObject,key)if('value'indesc){proxyObject[keyaskeyofT]=initialObject[keyaskeyofT]}(baseObject,key,desc)})returnproxyObject}
snapshot

要点:

返回一个只读不可变的对象,本质是调用createSnapshot函数

如何实现对象不可变的?

preventExtensions阻止对象修改

definePropertywritable属性默认为false

js复制代码createSnapshot=(target,version,handlePromise=defaultHandlePromise)={constcache=(target)if(cache?.[0]===version){returncache[1]}constsnap=(target)?[]:((target))markToTrack(snap,true)//(target,[version,snap])(target).forEach((key)={if((snap,key)){//}constvalue=(target,key)const{enumerable}=(target,key,)constdesc={value,enumerable:enumerable,configurable:true,}if((value)){markToTrack(value,false)//marknottotrack}elseif(valueinstanceofPromise){=()=handlePromise(value)}elseif((value)){const[target,ensureVersion]=(value)=createSnapshot(target,ensureVersion(),handlePromise,)}(snap,key,desc)})(snap)},
useSnapshot

只能在react框架中使用,因为它使用了react的useSyncExternalStore,关于useSyncExternalStore参见zustand一节

作用:

跟snapshot一样,都是用来获取不可变state的

区别是,snapshot是只要代理对象或者子代理对象改变都会创建,而useSnapshot是在组件里调用的,只有当组件重新渲染时才会重新创建

js复制代码exportfunctionuseSnapshot(proxyObject,options){constnotifyInSync=options?.syncconstlastSnapshot=useRef()constlastAffected=useRef()letinRer=trueconstcurrSnapshot=useSyncExternalStore(useCallback((callback)={constunsub=subscribe(proxyObject,callback,notifyInSync)callback()returnunsub},[proxyObject,notifyInSync],),()={constnextSnapshot=snapshot(proxyObject,use)try{if(!!isChanged(,nextSnapshot,,newWeakMap(),)){//}}catch(e){//ignoreifapromiseorsomethingisthrown}returnnextSnapshot},()=snapshot(proxyObject,use),)inRer=falseconstcurrAffected=newWeakMap()useEffect(()={==currAffected})if(?.MODE!=='production'){//eslint-disable-next-linereact-hooks/rules-of-hooksuseAffectedDebugValue(currSnapshot,currAffected)}constproxyCache=useMemo(()=newWeakMap(),[])//per-hookproxyCache//创建proxy,先查cache,没有降级到newProxyreturncreateProxyToCompare(currSnapshot,currAffected,proxyCache,targetCache,)}
订阅subscribe
js复制代码exportfunctionsubscribe(proxyObject,callback,notifyInSync){constproxyState=(proxyObject)//省略环境判断letpromiseconstops=[]//定义一个addListener,用来在代理状态中添加监听器,以便在代理对象发生更改时调用listenerconstaddListener=proxyState[3]letisListenerActive=falseconstlistener=(op)={(op)if(notifyInSync){callback((0))return}if(!promise){promise=().then(()={promise=undefinedif(isListenerActive){callback((0))}})}}constremoveListener=addListener(listener)isListenerActive=truereturn()={isListenerActive=falseremoveListener()}}
Rematch简介

是什么?

个人理解:对Redux框架的重封装,使得Redux更易用

Rematch和Redux的对比:

Rematch
移除了
Redux
中的这些东西:

声明action类型

action创建函数

thunks

store配置

mapDispatchToProps

sagas

Rematch的工作流程:

首先,用户通过view组件调用dispatch触发reduceraction

reduceraction会去修改state,并返回新的state

state改变触发组件重新渲染

Rematch的特点:

更合理的数据结构设计,rematch使用model的概念,整合了state,reducer以及effect

移除了redux中大量的常量以及分支判断

更简洁的API设计,rematch使用的是基于对象的配置项,更加易于上手

更少的代码

原生语法支持异步,无需使用中间件。

提供插件机制,可以进行定制开发

Rematch的使用单独使用Rematch

举个在小程序中使用的例子:

定义model

js复制代码constgift={name:'gift',state:{name:'',time:3000,price:99,},reducers:{changeName(state){return{state,name:`${newDate().getTime()}`}}},//定义异步actioneffects:{asyncchangeNameDelay(){awaitnewPromise(resolve=setTimeout(()={resolve()},10000))()}}}exportdefaultgift

初始化store

js复制代码import{init}from'@rematch/core';importcountfrom'./models/count';importgiftfrom'./models/gift';conststore=init({models:{count,gift}})exportdefaultstore

dispatchaction更新view

js复制代码importstorefrom'../../store/rematch'Page({data:{gift:{}},onLoad(){constfn=()={const{gift}=()('reducer',gift)({gift})}fn()(()={('change')fn()})},changeName(){()},changeNameDelay(){()}})
在React框架中使用Rematch

是否在react框架中使用只有视图层的区别,在react框架中使用时,仍旧可以使用现成的react-redux库包裹组件

在Rematch中使用插件

插件可以扩展Rematch功能,重写配置,添加新的model,甚至替换整个store。内置的的dispatch和effects也都是插件,分别用来增强dispatch和处理异步操作。除此之外,Rematch还开发了不少第三方插件,如:

@rematch/immer:对于一个复杂的对象,immer会复用没有改变的部分,仅仅替换修改了的部分,相比于深拷贝,可以大大的减少开销

@rematch/select
:给
Rematch
使用的
selectors
插件

在Model中增加了一个selectors属性,同时导出了一个select函数,挂在RematchStore上

Selector主要用于封装从state中查找特定值的逻辑、派生数据的逻辑以及通过避免不必要的重新计算来提高性能

@rematch/loading:对每个model中的effects自动添加loading状态

@rematch/updated:用于在触发effects时维护时间戳,主要用来throttleeffects

@rematch/persist
:使用
localstorage
选项提供简单的
redux
状态持久化。

和React,PersistGate一起使用,在等待数据从storage中异步加载的同时显示loading指示器

@rematch/typed-state
:在运行时进行类型检查

使用prop-types描述类型

举个例子,如何使用@rematch/immer插件

js复制代码import{init}from'@rematch/core';importimmerPluginfrom'@rematch/immer'importcountfrom'./models/count';importgiftfrom'./models/gift';constimmer=immerPlugin()conststore=init({models:{count,gift},plugins:[immer]})exportdefaultstore//:{changeName(state){=`${newDate().getTime()}`returnstate}}
Rematch的实现

从上面的使用方式可以看出,核心是使用init生成store,在调用{modelName}.{reducerName}

你是否在学习rematch的时候,有这几个问题:

Rematch是如何减少模板代码的,即如何自动生成actionType的?

既然没有改写Redux,那么Rematch如何和Redux结合的?

Rematch如何处理异步action?

插件机制如何实现?

带着问题,让我们一起看下rematch源码

init

主要有两步:

createConfig生成配置对象

生成一个自增的name,如果没有传入name,就使用这个自增的name

createRematchStore生成最后的rematchStore,rematchStore既包含了原本的方法,又在它的基础上做了扩展

model中的reducer、effects绑定到dispatch上

调用dispatch时会自动拼装

对于传入Rematch的插件,会在Rematchstore的初始化的各个阶段去调用生命周期钩子。forEachPlugin是插件机制的核心,通过这个方法,执行插件钩子函数

js复制代码exportconstinit=(initConfig)={//根据传入的参数,生成最后的配置对象constconfig=createConfig(initConfig||{})returncreateRematchStore(config)}exportdefaultfunctioncreateConfig(initConfig){conststoreName=??`RematchStore${count}`count+=1constconfig={name:storeName,models:||{},plugins:||[],redux:{reducers:{},rootReducers:{},enhancers:[],middlewares:[],,devtoolOptions:{name:storeName,(?.devtoolOptions??{}),},},}//验证config参数是否合法validateConfig(config)//Applychangestotheconfigrequiredbyplugins//省略配置插件部分代码((plugin)={//})returnconfig}functioncreateRematchStore(config){//setuprematch'bag'forstoringusefulvaluesandfunctionsconstbag=createRematchBag(config)//(createEffectsMiddleware(bag))//('createMiddleware',(createMiddleware)={(createMiddleware(bag))})constreduxStore=createReduxStore(bag)letrematchStore={reduxStore,name:,addModel(model){validateModel(model)createModelReducer(bag,model)prepareModel(rematchStore,model)enhanceModel(rematchStore,bag,model)(createRootReducer(bag))({type:'@@redux/REPLACE'})},}addExposed(rematchStore,)((model)=prepareModel(rematchStore,model))((model)=enhanceModel(rematchStore,bag,model))('onStoreCreated',(onStoreCreated)={rematchStore=onStoreCreated(rematchStore,bag)||rematchStore})returnrematchStore}
createRematchStore

主流程:

调用
createRematchBag,createRematchBag
会返回一个包含
models、reduxConfig、forEachPlugin
的对象。

models:[{name,reducers,}]

reduxConfig:redux配置相关

forEachPlugin:传入两个参数,method和fn,如果能找到method,则执行fn([method])

createEffectsMiddleware:添加处理effects的中间件,下面会详细介绍

forEachPlugin('createMiddleware',cb)

createMiddleware
来自
Rematch
提供的插件
plugins/typed-state
,如没有配置则不会执行

createMiddleware函数的返回值又是一个Redux的中间件

createReduxStore:核心,根据传入的配置,创建reduxstore

prepareModel:往最终返回的store对象暴露dispatch钩子

onStoreCreated:调用插件的onStoreCreated钩子,@rematch/persist插件的

createEffectsMiddleware

其实就是一个effects的Redux中间件

js复制代码functioncreateEffectsMiddleware(bag){//store,next,action分别对应redux的store,dispatch,actionreturnstore=next=action={if(){next(action)[](,(),,)}returnnext(action)}}
createReduxStore

流程:

遍历models执行createModelReducer

创建rootReducer

应用中间件

生成enhancers

生成初始化state,initialState

调用redux的createStore,传入2,4,5步生成的参数创建最终的store

createModelReducer

通过这个函数,我们可以看到,rematch是怎么无需定义actiontype的

js复制代码functioncreateModelReducer(bag,model){constmodelReducers={}constmodelReducerKeys=()((reducerKey)={//如果reducerkey含‘/',直接使用reducerkey作为actionName,否则使用`${}/${reducerKey}`作为actionnameconstactionName=isAlreadyActionName(reducerKey)?reducerKey:`${}/${reducerKey}`modelReducers[actionName]=[reducerKey]})//如果当前的action存在于model的reducer中,则直接执行这个reducer,否则则返回stateconstcombinedReducer=(state,action)={if(){returnmodelReducers[](state,,)}returnstate}constmodelBaseReducer=//Rematch可以和原有的redux的reducer同时存在,且=!modelBaseReducer?combinedReducer:(state=,action)=combinedReducer(modelBaseReducer(state,action),action)//如果插件中配置了onReducer事件,则依次执行('onReducer',(onReducer)={reducer=onReducer(reducer,,bag)||reducer})[]=reducer}

生成enhancers

如果init时传入了redux的devtoolComposer,则使用传入的

未传入的话,会先判断以下三个条件是否满足:

处于浏览器环境

安装了ReduxDevtools

未禁用ReduxDevtools满足之后,如果初始时传入devtoolOptions,则会开启ReduxDevtools

js复制代码constenhancers=?(,middlewares):composeEnhancersWithDevtools()(,middlewares)functioncomposeEnhancersWithDevtools(devtoolOptions={}){return!==='object'window.**REDUX_DEVTOOLS_EXTENSION_COMPOSE**?window.**REDUX_DEVTOOLS_EXTENSION_COMPOSE**(devtoolOptions):}
prepareModel

rematch派发action的时候是使用{modelName}.{reducerNameoreffectsName},prepareModel就是实现这一步的

具体流程:

创建一个空对象

将作为key,在dispatch上绑定这个空对象

遍历model上的所有reducer,通过createActionDispatcher将actionDispatcher作为value绑定上去

js复制代码functionprepareModel(rematchStore,bag,model){constmodelDispatcher={}[`${}`]=modelDispatchercreateDispatcher(rematchStore,bag,model)('onModel',(onModel)={onModel(model,rematchStore)})}functioncreateDispatcher(rematchStore,bag,model){constmodelDispatcher=[]constmodelReducersKeys=()((reducerName)={validateModelReducer(,,reducerName)modelDispatcher[reducerName]=createActionDispatcher(rematch,,reducerName,false)})if(){effects===='function'?():}consteffectKeys=(effects)((effectName)={validateModelEffect(,effects,effectName)[`${}/${effectName}`]=effects[effectName].bind(modelDispatcher)modelDispatcher[effectName]=createActionDispatcher(rematch,,effectName,true)})}constcreateActionDispatcher=(rematch,modelName,actionName,isEffect)={((payload,meta)={constaction={type:`${modelName}/${actionName}`}if(typeofpayload!=='undefined'){=payload}if(typeofmeta!=='undefined'){=meta}(action)},{isEffect,})}
结论

问题一:Rematch是如何减少模板代码的,即如何自动生成actionType的?
createModelReducer时,根据reducerkey,如果reducerkey包含'/',则将reducerkey作为actiontype,否则将{modelname}/{reducerkey}作为actiontype

问题二:既然没有改写Redux,那么Rematch如何和Redux结合的?
根据传入的config参数重组,最后生成reduxcreateStore所需的参数,本质上还是创建reduxstore,只不过经过一层封装

问题三:Rematch如何处理异步action?
createEffectsMiddleware用来处理异步action也就是effects,本质上还是创建redux的异步中间件

问题四:插件机制如何实现?
init时根据传入的plugin配置生成config对象,创建rematchstore时,通过forEachPlugin处理plugin的钩子函数

对比(省流篇)

redux

rematch

mobx

zustand

valtio

star

60.3k

8.5k

27.1k

41.2k

8.3k

大小(压缩前)

290k

312k

4.19M

327k

312k

诞生时间

2011

2018

2018

2019

2021

最近更新时间

3月前

2年前

4月前

17天前

17天前

原理

Flux思想发布订阅模式

对redux的二次封装

观察者模式基于数据代理

Flux思想观察者模式

基于数据代理数据双向绑定发布订阅模式

优点

1.通用的状态解决方案2.单一数据源,使得数据发生改变时更容易追踪3.生态系统完善4.函数式编程,在reducer中,接受输入,然后输出,不会有副作用发生,幂等性

1.更合理的数据结构设计,rematch使用model的概念,整合了state,reducer以及effect2.移除了redux中大量的常量以及分支判断3.更简洁的API设计,rematch使用的是基于对象的配置项,更加易于上手4.更少的代码5.原生语法支持异步,无需使用中间件。6.提供插件机制,可以进行定制开发

1.使用简单,上手门槛低2.通用的状态解决方案3.支持计算属性

1.不需要使用contextprovider包裹你的应用程序2.可以做到瞬时更新(不引起组件渲染完成更新过程)3.不依赖react上下文,引用更加灵活4.当状态发生变化时重新渲染的组件更少5.集中的、基于操作的状态管理6.基于不可变状态进行更新,store更新操作相对更加可控

1.容易使用和理解:没有复杂的概念,只有两个核心方法proxy和useSnapshot2.细粒度渲染:使用useSnapshot可以只在状态变化的部分触发组件的重新渲染

缺点

1.学习成本高,需要学习dispatch,action,reducer的概念2.使用起来比较复杂,需要定义大量的action3.在非react框架中使用时,默认只要store的任一属性发生改变,都会执行全部的订阅函数(通过订阅的函数),业务在使用时,需要自己实现diff更新

1.在非react框架中使用时,需要使用subscribe去订阅store的更新,但是只要store任一属性发生改变,都会引起更新,造成不必要的性能损耗。可以参照react-redux实现二次封装组件

1.可变状态模型,某些情况下可能影响调试2.体积较大3.在非react框架中使用时,默认只要观测值的任一属性发生改变,都会执行autorun,在使用时,需要二次封装,进行优化,减少性能消耗4.太过灵活,更容易导致bug

1.框架本身不支持computed属性,但可基于middleware机制通过少量代码间接实现computed

1.可变状态模型,某些情况下可能影响调试

学习成本

使用成本

异步支持

借助中间件

友好

友好

友好

友好

易于调试

性能

中等

中等

中等

Typescript友好

支持

支持

支持

支持

支持

版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。

相关推荐