React 组件之间的数据通信在 初探 React 组件 有初步接触,父组件可以通过声明 props
来向子组件传递数据,但是子组件无法向父组件传递数据,兄弟组件之间也不能相互传递数据。React 的这种单向数据流的通信模式能确保数据的流动简单可控,非常严谨,React 也一直秉承着简单严谨的设计思想。
那如上提到的非父子组件的数据通信该怎么办呢?有几种方案:
回调函数 父组件注册回调函数,子组件执行回调函数,这种方案比较简单,适用范围也有限,适合简单的子组件向父组件的数据通信;
事件 事件通信是 JavaScript 最常用的通信方案,这种通信方案不用局限于组件之间的关系,比较灵活,但是当应用复杂庞大的时候,使用这种通信方式会导致管理混乱,不可控;
Flux Facebook 官方团队提出来专门应用于 React 的一套应用的架构模式,本文将要讲述的就是该方案。
React 专注在 UI 层上,从 MVC
分层的架构模式上来看,React 主要涉及到的是 V
,一个 React 的组件可以是一个 Views
或 Controller-views
的混合体,它完全没有 M
层。Facebook 官方团队并没有照搬比较流行的 MV*
,而是提出了更适合 React 的 Flux
架构模式。
Flux 主要包含四个部分:
Action
)分发中心,用于分发动作给数据存储(Store
)中心;View
)发出通知;Action
,接收来自 Store
的通知;Store
中存储了组件的数据,要更新数据必须发起一个 Action
,不能直接去修改,要获取数据,也只能从 Store
中获取,因此 Flux 的数据流也是单向的。而 Dispatcher
只是一个分发器,将 Action
分发到 Store
中。
考虑到 Demo 代码体积较大,不能一一在文章中展现,我在 github 上传了 flux-demo,可以对照 demo 看看。
Demo 中有 3 个组件,FluxDemo 是入口组件,它包含了 2 个子组件 Dropdown 和 Content。Dropdown 组件在选中了某一项时会发起一个请求,请求的数据会在 Content 中展现出来。
接下来将着重讲解 Flux 的应用部分。
官方的 Flux 提供了一个简单的 Dispatcher
库,它有几个简单的 API,最常用的是 register
和 dispatch
。一个应用只需要一个 Dispatcher
中心,用于管理分发整个应用的 Action
。
register
用于在 Store
中注册回调,并且会返回一个 token
值,这个 token
可以用于卸载回调,还有一个很重要的功能就是用于 waitFor
。
dispatch
用于在 Action
中分发动作,但是这个动作的分发是同步的,如果想支持异步的动作分发可以对 Dispatcher
进行扩展,查看dispatcher/dispatcher.js。
// dispatcher/dispatcher.js
class Dispatcher extends Flux.Dispatcher {
constructor (...args) {
super(...args);
}
dispatch (type, action = {}) {
if (!type) {
throw new Error('You forgot to specify type.');
}
super.dispatch({type, ...action});
}
dispatchAsync (url, types, action = {}) {
const { request, success, failure } = types;
// 使用fetch来获取数据
const promise = fetch(url).then((response) => response.json());
// 分发请求开始的动作
this.dispatch(request, action);
promise.then(
// 分发请求成功的动作
(response) => {
this.dispatch(success, {...action, response});
},
// 分发请求错误的动作
(error) => {
this.dispatch(failure, {...action, error});
}
);
}
};
export default new Dispatcher();
与服务器的数据交互都是异步的,当我们在分发一个异步动作的时候,这个动作可以拆分成 3 个动作:
一个组件可以对应一系列的 Actions
,建议以组件来划分 Actions
的粒度。在 Demo 中有一个向服务器请求数据的 Action
,查看action/flux-demo.js。
// action/flux-demo.js
import dispatcher from '../dispatcher/dispatcher';
const fluxDemoActions = {
fetchIntroduction (name, path) {
const url = `http://127.0.0.1:3002${path}`;
// 一个异步Action拆分成了3个不同状态的Action
dispatcher.dispatchAsync(url, {
request: 'FETCH_INTRODUCTION',
success: 'FETCH_INTRODUCTION_SUCCESS',
failure: 'FETCH_INTRODUCTION_ERROR'
},{
name
});
}
};
export default fluxDemoActions;
每一个动作都要有一个唯一的动作类型,在上面的代码中,它定义了 3 个动作类型:
FETCH_INTRODUCTION
请求开始FETCH_INTRODUCTION_SUCCESS
请求数据成功FETCH_INTRODUCTION_ERROR
请求数据失败这 3 个动作在 Stores
可以有相对应的动作处理函数。
Stores
部分要复杂一些。Stores
的数据变更需要发送和接收通知,故需要用到事件系统,可以直接使用 EventEmmiter
模块,可以创建一个工具函数,用于创建 Store
。
const createStore = (methods) => {
let name;
class Store extends EventEmitter {};
for (name in methods) {
Store.prototype[name] = methods[name];
}
return new Store();
};
组件不能直接访问 Store
中存储的数据,如果能直接访问也意味着可以直接修改数据,为了确保数据流是单向的,这是不允许的,必须通过 Store
本身提供的数据访问接口来获取数据,最后,需要在 Store
中注册一些与 Action
中相对应的回调函数。
// 数据存储对象,外部不能直接访问
const data = {
introduction: {}
};
// 创建store
const fluxDemoStore = createStore({
getIntroduction (name) {
return data.introduction[name];
}
});
// 注册回调函数的时候会返回一个token
fluxDemoStore.dispatchToken = dispatcher.register((action) => {
switch(action.type) {
case 'FETCH_INTRODUCTION':
// do something
break;
case 'FETCH_INTRODUCTION_SUCCESS':
// 请求成功后,将数据存储到私有的对象中
data.introduction[action.name] = action.response.data;
// 同时通过事件系统发送数据变更的通知
fluxDemoStore.emit('change');
break;
case 'FETCH_INTRODUCTION_ERROR':
// do something
break;
}
});
export default fluxDemoStore;
这里需要注意的是,在注册回调函数时用到了 switch
,分发任何一个 Action
时,Store
中的回调函数就通过 action.type
来区分到底分发的是哪一个 Action
,查看store/flux-demo.js。
上面把 flux 部分都分析完后,还是回到组件层面。
Dropdown 组件在选择了某一项时,会触发一个请求数据的 Action
。如果想让一个组件尽量能复用,那么它最好不要和 flux 有关联,这样能确保组件的独立性,所以在选择的时候提供一个回调函数,那么 Action
就可以在回调函数中调用,查看component/flux-demo.js。
selectCallback = (name) => {
const path = `/api/${name.toLowerCase()}`;
this.setState({
selected: name
}, () => {
// 发起请求数据的Action
fluxDemoActions.fetchIntroduction(name, path);
});
}
那么该在哪个组件中获取数据更好呢?首先,消费数据的是 Content 组件,最简单的方案就是直接在 Content 组件中来监听数据的变化更新数据。当然也可以在父组件 FluxDemo 中进行,然后再通过声明 props
来传递过去。一个简单的划分原则就是看消费数据的组件是否需要复用,如果为了复用,要确保其通用性,肯定是不包含 flux 部分更好,而如果不会有复用,那么直接在组件内获取数据更好,因为数据少了一层传递,查看component/content.js
// content.js
import fluxDemoStore from '../store/flux-demo';
class Content extends React.Component {
constructor (props) {
super(props);
this.state = {};
}
// 组件装载完毕则绑定数据更新的事件
componentDidMount () {
fluxDemoStore.on('change', this.refreshContent);
}
// 组件卸载的时候同时卸载事件
componentWillUnmount () {
fluxDemoStore.off('change', this.refreshContent);
}
refreshContent = () => {
// 从store获取数据
const introduction = fluxDemoStore.getIntroduction(this.props.name);
// 获取到数据通过setState来更新组件的状态
this.setState({
introduction
});
}
render () {
return (
<div>
<p>{this.state.introduction}</p>
</div>
);
}
};
export default Content;
Flux 的架构模式初次接触的话可能不是那么容易消化,可以仔细看看 Demo,然后自己尝试去写一个应用,你就会真正理解 Flux 单向数据流的魅力。
我建立了一个 React 技术交流群,欢迎大家入群一起交流,群号:471410265。
Ballade: 重新诠释 Flux 架构
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8