为了防止被打,有请“燕双鹰”镇楼️‍♀️️‍️‍...o...

话说新冠3年,“状态管理框架”豪杰并起、群雄逐鹿,ReduxToolkit、Mobx、Vuex、Pinia、Dva、Rematch、Recoil、Zustand、Mirror...敢问英雄独钟哪厢?

Flux状态管理

笔者也用过很多态管理框架,大部分都是Flux框架的变种,只不过加上了一些自己的糖衣和辅助方法。

  • 只要糖衣做得好,省时省力人人要!
  • 后面随着Typescript的普及,自动类型推断也是状态管理框架易用性的重要指标。

我们先简单回顾几款最主流的Flux状态管理框架的写法:

//基于Redux的Dva:
{
state(){
return {curUser: null}
},
reducers: {
setUser(state, {payload}) {
return {...state, curUser: payload}
},
},
effects: {
*login({ payload: {username, password} }, { put, call }){
const { data } = yield call(api.login, username, password);
yield put({ type: 'setUser', payload: data }); //无TS类型提示
}
}
}; //Vuex:
{
state(){
return {curUser: null}
},
mutations: {
setUser(state, curUser) {
state.curUser = curUser;
}
},
actions: {
async login({ commit }, {username, password}) {
const { data } = await api.login(username, password);
commit('setUser', data) //无TS类型提示
}
}
} //Pinia:
{
state(){
return {curUser: null}
},
actions: {
setUser(curUser) {
this.curUser = curUser;
}
async login(username, password) {
const { data } = await api.login(username, password);
this.setUser(data) //有TS类型提示
}
}
}

果然都是一个妈生的,本质上无非就是玩3个概念:

  • State
  • 同步Action
  • 异步Action

为Flux再添一把火

既然都是玩这3个概念,大家都容易理解,那么要自荐的Elux就要闪亮登场了:

先看它的基本用法:

class Model{
onMount() {
//初始赋值State
this.dispatch(this.actions._initState({curUser: null}));
} @reducer //类似Vuex的mutations
setUser(curUser) {
//react中必需返回一个新state
//return {...this.state, curUser};
this.state.curUser = curUser;
} @effect() //类似Vuex的action
async login(username, password) {
const { data } = await api.login(username, password);
await this.dispatch(this.actions.setUser(data));
this.getRouter().relaunch({url: HomeUrl});
}
}
  • onMount:初始化钩子,在其中完成State的初始赋值。
  • reducer:React系很容易理解,Vue系可以理解为mutation,它是改变State的唯一途径。
  • effect:React系很容易理解,Vue系可以理解为action,它是异步Action。

所以从糖衣语法来说Elux其实与Dva/Vuex/Pinia也差不多,不同在于:

  • Elux使用Decorator装饰器语法来定义reducer(mutation)effect(action),这样更简洁。
  • Elux使用Class来组织Model,有2点好处:
    • 可以通过类的继承和多态来复用公共逻辑。
    • 可以通过TS的类成员权限(public/private/protected)来更好的封装。

Elux特性

除了糖衣语法,Elux还有其更深层次的创新:

从图中可以看出:

  • store中保存了所有state
  • 每个Model管理store下的一个节点
  • view从store中获取state
  • dispatch(action)是触发reducer/effect的唯一途径
  • reducer是纯函数,也是修改state的唯一途径
  • effect可以处理任何异步操作,但不能直接修改state
  • 一个action的派发类似于事件,可以触发多个reducer和effect监听
  • view/effect/router都可以派发action

自动生成Action

这点类似于Pinia,不需要手动盲写类似于{type:"xxx.xxx",payload:xxxx}这样的Action结构体,而是通过方法自动生成:

const loginAction = stageActions.login('admin','123456');
//等于{type: 'user.login', payload:{username:'admin', password:'123456'}}
dispatch(loginAction);

且具备完美的TS类型提示:

模块化

Elux使用微模块来组合应用,每个微模块对应一个业务模型Model,每个Model使用reducer/effect来维护Store下的一个节点ModuleState

微模块是一种前端业务模块化方案,至此不引申开来,可参见我的发文【微模块-前端业务模块化探索,拆解巨石应用的又一利器

事件化

action当做Model中的事件,将reducereffect当做Handler,这意味着dispatch(action)可以触发多个reducer和effect。

通过事件总线机制,在保持各Model松散性的同时,加强Model之间的协同交互,举个例子:

假设有3个模块:user(用户模块)、article(文章模块)、my(个人中心模块)

当用户登录时,article(文章模块)需要将状态修改为可编辑,my(个人中心模块)需要获取最新通知

user/model.ts中编写登录逻辑:

// src/modules/user/model.ts

export class Model extends BaseModel<ModuleState> {
@reducer
public setUser(curUser: User) {
this.state.curUser = curUser;
}
@effect()
public async login(username: string, password: string) {
const { data } = await api.login(username, password);
await this.dispatch(this.actions.setUser(data));
this.getRouter().relaunch({url: HomeUrl});
}
}

article/model.ts中通过reducer监听setUserAction

// src/modules/article/model.ts

export class Model extends BaseModel<ModuleState> {
@reducer
public ['user.setUser'](curUser: User) {
//根据当前用户是否登录来决定是否可编辑
this.state.editable = curUser.hasLogin;
}
}

my/model.ts中通过effect监听setUserAction

// src/modules/my/model.ts

export class Model extends BaseModel<ModuleState> {
@reducer
public updateNotices(notices: Notices[]) {
this.state.notices = notices;
}
@effect()
public async ['user.setUser'](curUser: User) {
if(curUser.hasLogin){
const notices = await this.api.getNotices();
this.dispatch(this.actions.updateNotices(notices));
}
}
}

user/views/Login.tsx中派发loginAction

// src/modules/user/views/Login.tsx

export default ({dispatch}) => {
const login = () => {
dispatch(userActions.login('admin', '123456'));
};
return (
<div>
<button onClick={login} >登录</button>
</div>
);
}

统一化

数据模式有2大基本阵营:ImmutableData 和 MutableData。Redux是ImmutableData阵营的代表;Vue为MutableData的代表。

Elux可以同时兼容这2种数据模式,它们的唯一区别在reducer中:

  • ImmutableData:要求返回一个新数据,不可以修改原数据。
  • MutableData:可以直接修改原数据。
class Model{
@reducer
setUser(curUser) {
//vue中可以直接修改state:
this.state.curUser = curUser;
//react中必需返回一个新state
//return {...this.state, curUser};
}
}

当然,在MutableData模式下,返回一个新数据也是可以的,这为跨React和Vue项目共享Model提供了解决方案。

await dispatch

actionHander中如果有异步操作,将返回一个promise,可以await其执行,例如:

// src/modules/user/views/Login.tsx

const onSubmit = (values: HFormData) => {
const result = dispatch(userActions.login(values));
result.catch(({message}) => {
//如果出错(密码错误),在form中展示出错信息
form.setFields([{name: 'password', errors: [message]}]);
});
};

跟踪effect执行情况

通常effect中包含异步操作,对于异步操作我们通常都需要显示Loading,Elux中可以很方便的跟踪它的执行情况,只需要在装饰器effect()中传入Loading状态Key名即可。

  • @effect('this.loginLoading'):表示将执行情况注入this.state.loginLoading
  • @effect() 不传参数等于@effect('stage.globalLoading'):表示将执行情况注入stage.state.globalLoading
  • @effect(null):参数为null表示不跟踪执行情况
// src/modules/user/model.ts

export class Model extends BaseModel<ModuleState> {

  @effect('this.loginLoading') //将该方法的执行情况注入this.state.loginLoading中
public async login(username: string, password: string) {
const { data } = await api.login(username, password);
await this.dispatch(this.actions.setUser(data));
this.getRouter().relaunch({url: HomeUrl});
}
}

在View中使用loginLoading状态

// src/modules/user/views/Login.tsx

export default ({dispatch, loginLoading}) => {
return (
<div>
<button onClick={login} disable={loginLoading==='Start'} >登录</button>
</div>
);
}

自动合并和维护Loading队列

不仅可以很方便的跟踪和注入loading状态,框架还自动维护loading队列,比如相同Key名的多笔loading状态将自动合并成队列管理(队列中的任务全部完成即改变loading状态)。

自动区分浅度Loading和深度Loading

export type LoadingState = 'Start' | 'Stop' | 'Depth';

比如不超过1秒的loading为浅度Loading,否则为深度Loading,这样区分的好处是:对于浅度Loading只需要防止用户重复点击,视觉上用户不用感知,否则会出现一闪而过的Loading界面,反而会影响用户体验。

const Component: FC<Props> = ({loadingState}) => {
return (
<div className="global-loading">
{loadingState === 'Depth' && <div className="loading-icon" />}
</div>
);
};

方便的错误处理

effect执行中出现任何失败或者错误,都将自动派发一个stage._error的内置action,可以监听它来集中处理错误:

// src/modules/stage/model.ts

export class Model extends BaseModel<ModuleState> {
@effect(null)
protected async ['this._error'](error: CustomError) {
if (error.code === CommonErrorCode.unauthorized) {
this.getRouter().push({url: '/login'}, 'window');
}else{
alert(error.message);
}
throw error;
}
}

支持泛监听

可以使用一个Hander监听多个Action:

  • 使用,符号分隔多个actionType
  • 使用*符号作为moduleName的通配符
  • 使用this可以指代本模块名
class Model extends BaseModel
@effect()
//同时监听2个模块的'_initState'
async ['moduleA._initState, moduleA._initState'](){
console.log('moduleA/moduleB inited');
}
@effect()
//同时监听所有模块的'_initState'
async ['*._initState'](){
console.log('all inited');
}
}

还可以路由守卫

Elux中的路由发生跳转时会自动派发几个内置的action:

  • stage._testRouteChange:是否允许本次跳转。你可以监听它,阻止路由跳转:
    export class Model extends BaseModel<ModuleState> {
    private checkNeedsLogin(pathname: string): boolean {
    return pathname.startsWith('/admin/')
    }
    @effect(null)
    protected async ['this._testRouteChange']({url, pathname}) {
    if (!this.state.curUser.hasLogin && this.checkNeedsLogin(pathname)) {
    throw new CustomError(CommonErrorCode.unauthorized, '请登录!');
    }
    }
    }
  • stage._beforeRouteChange:路由即将跳转。你可以监听它,执行某些逻辑...
  • stage._afterRouteChange:路由跳转完成。你可以监听它,执行某些逻辑...

多实例历史快照

  • 路由push时你可以将当前Store实例冻结起来,并保存在历史栈中。
  • 路由back时将自动激活之前被冻结的Store实例,快速恢复历史状态。

自动清理无用状态

传统全局Store有个很大的弊端,就是Store中的状态会不断累积,缺乏自动释放机制。比如当前路由从用户列表跳转到了文章列表,如果不主动操作,Store中的userList可能一直存在。

Elux改进了这个痛点,每次路由发生变化时都将创建一个空的Store,然后挑选出有用的状态重新挂载,这也相当于一种自动垃圾回收机制。

应用

Elux框架奉行轻UI、重Model领域驱动理念,推荐将业务逻辑UI逻辑剥离,进行抽象的业务逻辑建模,从而让业务Model可以跨框架、跨平台、跨工程复用。

而其内置的状态管理框架,有效的支撑了这一设计理念,更多信息参见:

最后

好了,感谢小伙伴们耐心看到这里,正如标题所言,如果还是觉得不好,现在可以来打我了,坐标:广西东兴,o友情提醒:泡面不要带少了哦...

最新文章

  1. Java中isAssignableFrom的用法
  2. hadoop入门(3)&mdash;&mdash;hadoop2.0理论基础:安装部署方法
  3. Python的平凡之路(15)
  4. wpf button的mouse(leftbutton)down/up,click事件不响应解决办法
  5. js运动
  6. MongoDB索引限制
  7. 今天用node的cheerio模块做了个某乎的爬虫
  8. 由HashMap哈希算法引出的求余%和与运算&amp;转换问题
  9. Address already in use: make_sock: could not bind to address 0.0.0.0:80
  10. 前端笔记之JavaScript(十一)event&amp;BOM&amp;鼠标/盒子位置&amp;拖拽/滚轮
  11. 索引优化原则及Oracle中索引总结
  12. C++标准库之右值引用相关:引用折叠
  13. HDU1711-KMP-水题
  14. Java泛型简单理解
  15. mysql查看和修改密码策略
  16. javaService
  17. 给Ubuntu替换阿里的源
  18. Linux目录结构及解释(附图)
  19. 图形学思考 - 聊聊透视图投射矩阵perspective projective matrix
  20. jQuery中resetForm与clearForm的区别?

热门文章

  1. Python:socket编程教程
  2. TypeScript ReadonlyArray(只读数组类型) 详细介绍
  3. 北京市行政村边界shp数据/北京市乡镇边界/北京市土地利用分类数据/北京市气象数据/降雨量分布数据/太阳辐射数据
  4. Tapdata 实时数据融合平台解决方案(三):数据中台的技术需求
  5. Graph Neural Networks:谱域图卷积
  6. 使用Java客户端发送消息和消费的应用
  7. NC15665 maze
  8. 解决Windows10、Windows11文件名无法大写的问题
  9. 我有 7种 实现web实时消息推送的方案,7种!
  10. 迷宫类dp整合