本文是一篇译文,英文好的同学可以阅读这里。
两个React组件之间如何通信?这是一个很好的问题,有很多种答案。这个取决于组件之间的关系,还有,取决于你更喜欢用哪种方式。
我在这里讲的不是使用数据存储方式在组件之间通信,我真的只是仅仅在谈如何在组件之间通信。
组件之间有三种可能的关系:#####
-> 表示谁向谁发送消息,例如A->B表示A发送消息给B
- 主->从(父->子)
- 从->主(子->父)
- 两者之间没有任何关联
父-子之间的通信方式:###
这个是一种最简单的情况,在React中很自然的一种使用方式并且你正在使用。
你有一个组件A需要和组件B通信,并且给它传递一些属性props。
var MyContainer = React.createClass({
getInitialState: function() {
return { checked: true };
},
render: function() {
return <ToggleButton text="Toggle me" checked={this.state.checked}/>;
}
});
var ToggleButton = React.createClass({
render: function() {
return <label>{this.props.text}: <input type="checkbox" checked={this.props.checked} /></label>;
}
});
这里<MyContainer> 的render方法生成一个<ToggleButton>,传递一个checked属性。这就是简单的通信。
你只需要给父组件正在rendering的子组件传递一个属性prop就可以了进行通信了。
顺便提一下,在这里例子中,注意一下点击checkbox 是没有效果的。
<ToggleButton>有一个<MyContainer>传递给它的属性checked,但它是没法改变这个属性的(属性checked是不可修改的)。
子-父之间的通信方式:###
现在,让我们来讨论下<ToggleButton>控制自己的状态并且想告诉父组件<MyContainer>我被点击了,通知父组件展示一些东西。所以,我们添加一个初始化状态和一个<ToggleButton>点击的事件:
var ToggleButton = React.createClass({
getInitialState: function() {
return { checked: true };
},
onTextChanged: function() {
console.log(this.state.checked); // it is ALWAYS true
},
render: function() {
return <label>{this.props.text}: <input type="checkbox" checked={this.state.checked} onChange={this.onTextChanged}/></label>;
}
});
注意一下我在onTextChanged方法中没有改变属性checked的值,它的值一致都是true。因为checkbox只会通知你它的状态改变了,但是不会告诉你它现在是选中还是没有选中。
所以我们在下面这个方法中增加了一行状态改变的代码,和一个待实现的通知父组件<MyContainer>的代码
onTextChanged: function() {
this.setState({ checked: !this.state.checked });
// callbackParent(); // ??
},
要拿到一个指向父组件的回调对象,我们准备使用在第一种关系父-子之间的通信方式中用到的通信方式。父组件<MyContainer>会通过prop传递一个回调给子组件<ToggleButton>:其实我们可以传递任何对象(属性、方法),他们不是DOM属性,他们只是简单的Javascript对象。
下面是一个例子,演示子组件<ToggleButton>如何通知父组件<MyContainer>它的状态发生了变化。父组件收到这个“变化事件”并且也改变它自己的状态来显示收到的变化:
var MyContainer = React.createClass({
getInitialState: function() {
return { checked: false };
},
onChildChanged: function(newState) {
this.setState({ checked: newState });
},
render: function() {
return <div>
<div>Are you checked ? {this.state.checked ? 'yes' : 'no'}</div>
<ToggleButton text="Toggle me" initialChecked={this.state.checked} callbackParent={this.onChildChanged} />
</div>;
}
});
var ToggleButton = React.createClass({
getInitialState: function() {
// we ONLY set the initial state from the props
return { checked: this.props.initialChecked };
},
onTextChanged: function() {
var newState = !this.state.checked;
this.setState({ checked: newState });
this.props.callbackParent(newState); // hey parent, I've changed!
},
render: function() {
return <label>{this.props.text}: <input type="checkbox" checked={this.state.checked} onChange={this.onTextChanged}/></label>;
}
});
这里是最终的效果:
当我点击选择框,父组件收到事件,页面显示为’yes‘.
以上的两种关系都存在一个问题,如果父-子组件之间有好几层其他的组件,那么我们就需要通过props一层层的传递我们的属性和方法,这将是一个灾难性的场景。
没有任何关系的组件之间通信方式:###
如果你的组件之间没有任何关联(或者有关联但是两者之间的隔着很多的组件并且你不想通过“中间的组件”去传递属性和方法),那么唯一的方式就是通过某些“事件”,一个组件去订阅这个事件,另外一个组件去触发这个事件。这是任何事件驱动的系统中最基本的两种操作:订阅或者监听一个事件,发送/触发/发布/调度这个事件去通知哪些订阅者。
下面介绍三种模式来处理事件,你可以点击这里比较它们。
-
Event Emitter/Target/Dispatcher :
需要从EventEmitter/EventTarget/EventDispatcher继承或者实现合适的接口myObject.addEventListener('myCustomEventTypeString', handler); myObject.dispatchEvent(new Event('myCustomEventTypeString')); myObject.removeEventListener('myCustomEventTypeString', handler);
一个非常简单的EventEmitter,如果没有过多的要求,它可以简单的这么写:
// 继承EventEmitter 去使用 this.subscribe 和 this.dispatch 这两个方法
var EventEmitter = {
_events: {},
dispatch: function (event, data) {
if (!this._events[event]) return; // no one is listening to this event
for (var i = 0; i < this._events[event].length; i++)
this._events[event][i](data);
},
subscribe: function (event, callback) {
if (!this._events[event]) this._events[event] = []; // new event
this._events[event].push(callback);
}
}
otherObject.subscribe('namechanged', function(data) { alert(data.name); });
this.dispatch('namechanged', { name: 'John' });
这是一个非常简单的EventEmitter ,但是它可以完成我们的需求。
当然你也可以使用Node.js中的EventEmitter,如果有不清楚的同学,请移步这里
-
Signals:
与Event Emitter/Target/Dispatcher 相比,不需要指定事件的“名称”,可以避免硬编码带来的问题myObject.myCustomEventType.add(handler); myObject.myCustomEventType.dispatch(param1, param2, ...); myObject.myCustomEventType.remove(handler);
React 团队使用的是: js-signals 它基于 Signals 模式,用起来相当不错。
这里简单介绍下使用方式
第一步:命令行切换到你的项目目录,执行
npm install signals
第二步:引入模块
const signals= require('signals');
第三步:使用方式
//custom object that dispatch a `started` signal
var myObject = {
started : new signals.Signal()
};
function onStarted(param1, param2){
alert(param1 + param2);
}
myObject.started.add(onStarted); //add listener
myObject.started.dispatch('foo', 'bar'); //dispatch signal passing custom parameters
myObject.started.remove(onStarted); //remove a single listener
-
Publish / Subscribe : 类似于很多语言中的事件总线EventBus广播的方式,相比EventEmitter,优点是组件之间可以完全独立,没有任何关联。React中比较常用的是库是PubSubJS,关于这个库的详细使用请查看官方的说明
这里简单介绍下使用方式
第一步:命令行切换到你的项目目录,执行
npm install pubsub-js
第二步:引入模块
const PubSub = require('pubsub-js');
第三步:订阅事件
// create a function to subscribe to topics var mySubscriber = function( msg, data ){ console.log( msg, data ); }; // add the function to the list of subscribers for a particular topic // we're keeping the returned token, in order to be able to unsubscribe // from the topic later on var token = PubSub.subscribe( 'MY TOPIC', mySubscriber );
第四步:发布事件
// 异步发布事件,非阻塞,无需等待订阅者处理结束 PubSub.publish( 'MY TOPIC', 'hello world!' ); // 同步发布事件,阻塞,需要等待订阅者处理结束 PubSub.publishSync( 'MY TOPIC', 'hello world!' );