React 文档对于组件通信一般只讲到了父子之间的通信,对于简单的应用来说已经是够用了。但是,对于大型应用来说就有点捉襟见肘了,如果还用简单的绑定函数和属性,在关系比较弱的组件实现通信就变得很麻烦。这篇文章会从 EventHub 到 Redux 来讲述一种更好的通信实现方法。
例子
先从一个简单例子开始吧,假设现在有下图的组件结构:
- 每个组件旁边是当前这个家庭的总资产
- Son 2 组件有个 Pay 按钮,每次点击会扣 100,同时其他组件也会更新总资产
如果还是使父子通信,那么只能不断去传递总资产,十分麻烦。
EventHub
如果将这个例子看成真实的例子,我们可能一般会请一个管家来帮我们管理资产。比如:
- Son 2 想 Pay,那么先和管家说:“我想要 Pay 啦”
- 管家去扣总资产,再去告诉其他人现在总资产已经扣了 100 了,新资产是 xxxx
这个思想搬到代码里就是 EventHub。其主要的功能是就发布事件(on 监听)和订阅事件(trigger 触发)。一个简单的 EventHub 可以写成这样:
let callbackLists = {}
let eventHub = {
trigger(eventName, data) {
let callbackList = callbackLists[eventName]
if (!callbackList) {
return
}
for (let i = 0; i < callbackList.length; i++) {
callbackList[i](data)
}
},
on(eventName, callback) {
if (!callbackLists[eventName]) {
callbackLists[eventName] = []
}
callbackLists[eventName].push(callback)
}
}
Son1.on('pay', function (data) { nowData = data })
Son3.on('pay', function (data) { nowData = data })
Son4.on('pay', function (data) { nowData = data })
...
// Pay in Son 2
function pay () {
money.amount -= 100
eventHub.trigger('pay', 100)
}
- 一开始先用监听这个事件,将回调函数都 push 到数组中
- 每次触发事件,就将对应的事件回调全都执行一次
管家
但是这样用 EventHub 会使得每个组件都要去监听很麻烦,而且在 Son 2 里直接操作全局 money
不够优雅。所以这就需要上面提到的管家,监听和扣钱就全都让管家来做 。
- 首先管家先监听该事件
- 在
<App/>
中每个组件都传递money
,即<App money={money}/>
- 并在修改数据之后再去 render 组件来更新视图
let alfred = {
init() {
eventHub.on('Pay', function (data) {
money.amount -= data
render() /// Use DOM Diff algorithm to compare and render
})
}
}
alfred.init()
这样一来发布事件的时候,会更新全部的视图,每个组件里的金额也会跟着改变。
pay() {
eventHub.trigger('Pay', 100)
}
单向数据流
从上面我们可以发现这些数据都是单向流动的,如:
可以看到这种用法的特点:
- 所有的数据都放在顶层组件
- 所有动作通过事件来沟通
转成 Redux
其实 Redux 就是 EventHub 的思想,里面提到的 action,store,reducer 分别是 EventHub 里的 trigger 事件,全局数据,监听事件的回调代码。下面是简单的事例。
store
将全部的数据都用一个 store 存放着,这个 store 就是指全局的数据,由顶层组件拥有着。
// Data
let money = {
amount: 100000
}
let user = {
id: 1,
nickname: 'User'
}
let store = {
money: money,
user: user
}
action
想做一件事,就是一个动作,trigger 的动作就是 action。
pay() {
// Action
// Action Type: 'Pay'
// Payload: 100
eventHub.trigger('Pay', 100)
}
reducer
对数据的变动。
eventHub.on('Pay', function (data) { // subscribe
money.amount -= data // reducer
render() // Use DOM Diff algorithm to modify data
})
改写成 Redux
上面的代码很容易就改写成 Redux 的代码。
生成 store
先获取全局数据 store。
let createStore = Redux.createStore
let reducers = (state, action) => {
state = state || {
money: { amount: 10000 }
}
switch (action.type) {
default:
return state
}
}
const store = createStore(reducers)
console.log(store.getState())
传入顶层组件
function render() {
ReactDOM.render(
<App store={store.getState()}/>,
document.querySelector('#app')
)
}
发布事件
不同于我们的trigger
,这里用dispatch
,其实原理上来说都是一样的,不过是换了个单词。
pay() {
store.dispatch({ type: 'Pay', payload: 100 })
}
监听事件并修改数据
let reducers = (state, action) => {
state = state || {
money: { amount: 10000 }
}
switch (action.type) {
case 'Pay':
return {
money: {
amount: state.money.amount - action.payload
}
}
default:
return state
}
}
但是现在还没有更新,因为没有重新 render。
订阅
Redux 里一定要订阅才会对修改后的数据作视图的渲染。
function render() {
ReactDOM.render(
<App store={store.getState()}/>,
document.querySelector('#app')
)
}
render()
store.subscribe(render)
Redux 为什么这么啰嗦
其实个人觉得 EventHub 挺容易理解的,但是到了 Redux 这里就变难了。主要的原因是为了增加多点约束来保证代码质量。
约束事件名
为了防止写很多个事件名,所以将这些事件名要写成一个列表,这个列表就是 reducers
switch (action.type) {
case 'Pay':
...
case 'event1':
...
case 'event2':
...
case 'event3':
...
case 'event4':
...
case 'event5':
...
default:
}
约束全局数据访问方法
在重要的地方不能用 state,只能用 props
的形式用 state。不能直接改全局数据。
// Avoid modifying global data directly
this.props.money.amount -= 100
(完)