填坑之路:React状态管理
文中涉及的React demo代码使用了16.8的新增特性Hooks:
它可以让你在不编写class的情况下使用state以及其他的React特性。
前言
刚立项时,可能只有一个根组件Root:管你啥业务,一把梭。
项目慢慢有起色,好事者就拆出了一些子组件,必然,它们间将有一些数据流动,问题不大,可以让他们“父子相连”。
现在项目爆了,业务N倍增长,不得不拆出更多的子孙组件出来,实现更多复杂业务。
但愿逻辑比较简单,数据流动是一层层往下:
现实总是残酷的,往往结构都是这样的,父子孙组件间关系混乱:
怎么办???
方案
只要思想不滑坡,办法总比困难多:
- 方案1,梳理项目逻辑,重新设计组件(手动Fxxk!!!)
- 方案2,辞职,换个公司重开(手动狗头)
项目迭代过程中,不可避免出现组件间状态共享,而导致逻辑交错,难以控制。
我们会想:能不能有一种实践规范,将所有可能公用的状态、数据及能力提取到组件外,数据流自上往下,哪里需要哪里自己获取,而不是prop drilling,大概如下:
于是这样一种数据结构冒了出来:
const store = {
state: {
text: 'Goodbye World!'
},
setAction (text) {
this.text = text
},
clearAction () {
this.text = ''
}
}
外部变量store,其中state来存储数据,store里面有一堆功能各异的action来控制state的改变。
我们规定:只能通过调用action来改变state,于是我们就可以通过action清晰地掌握着state的动向,日志、监控、回滚等能力随着而来。
于是我们大概的看到了Flux的雏形。
Flux
2013年,Facebook亮出React的时候,也跟着带出的Flux。Facebook认为两者相辅相成,结合在一起才能构建大型的JavaScript应用。
做一个容易理解的对比,React是用来替换jQuery的,那么Flux就是以替换Backbone.js、Ember.js等MVC一族框架为目的。
如上图,数据总是“单向流动”,相邻部分不存在互相流动数据的现象,这也是
Flux一大特点。
-
View发起用户的Action -
Dispatcher作为调度中心,接收Action,要求Store进行相应更新 -
Store处理主要逻辑,并提供监听能力,当数据更新后触发监听事件 -
View监听到Store的更新事件后触发UI更新
感兴趣可以看看每个模块的具体含义:
Action
一个普通的
Javascript对象,一般使用type与payload描述了该action的具体含义。
在Flux中一般定义actions:一组包含派发action对象的函数。
// actions.js
import AddDispatcher from '@/dispatcher'
export const counterActions = {
increment (number) {
const action = {
type: 'INCREMENT',
payload: number
}
AddDispatcher.dispatch(action)
}
}
以上代码,使用counterActions.increment,将INCREMENT派发到Store。
Dispatcher
将Action派发到Store,通过flux提供的Dispatcher注册唯一实例。
Dispatcher.register方法用来登记各种Action的回调函数
import { CounterStore } from '@/store'
import AddDispatcher from '@/dispatcher'
AppDispatcher.register(function (action) {
switch (action.type) {
case INCREMENT:
CounterStore.addHandler();
CounterStore.emitChange();
break;
default:
// no op
}
});
以上代码,AppDispatcher收到INCREMENT动作,就会执行回调函数,对CounterStore进行操作。
Dispatcher只用来派发Action,不应该有其他逻辑。
Store
应用状态的处理中心。
Store中复杂处理业务逻辑,而由于数据变更后View需要更新,所以它也负责提供通知视图更新的能力。
因为其随用随注册,一个应用可以注册多个Store的能力,更新data flow为:
细心的朋友可以发现在上一小节CounterStore中调用了emitChange的方法,对,它就是用来通知变更的。
import { EventEmitter } from "events"
export const CounterStore = Object.assign({}, EventEmitter.prototype, {
counter: 0,
getCounter: function () {
return this.counter
},
addHandler: function () {
this.counter++
},
emitChange: function () {
this.emit("change")
},
addChangeListener: function (callback) {
this.on("change", callback)
},
removeChangeListener: function (callback) {
this.removeListener("change", callback)
}
});
以上代码,CounterStore通过继承EventEmitter.prototype获得触发emit与监听on事件能力。
View
Store中的数据的视图展示
View需要监听视图中数据的变动来保证视图实时更新,即
- 在组件中需要添加
addChangeListerner - 在组件销毁时移除监听
removeChangeListener
我们看个简单的Couter例子,感受下Flux的代码。
(手动分割线)
认真体验的朋友可能会注意到:
- 点击
reset后,store中的couter被更新(没有emitChange所以没实时更新视图); - 业务逻辑与数据处理逻辑交错,代码组织混乱;
好,打住,再看个新的数据流。