在React中最小的逻辑单元是组件,组件之间若是有耦合关系就会进行通讯,本文将会介绍React中的组件通讯的不一样方式javascript
经过概括范,能够将任意组件间的通讯归类为四种类型的组件间通讯,分别是父子组件,爷孙组件,兄弟组件和任意组件,
须要注意的是前三个也能够算做任意组件的范畴,因此最后一个是万能方法html
父子组件间的通讯分为父组件向子组件通讯和子组件向父组件通讯两种状况,下面先来介绍父组件向子组件通讯,
传统作法分为两种状况,分别是初始化时的参数传递和实例阶段的方法调用,例子以下前端
class Child { constructor(name) { // 获取dom引用 this.$div = document.querySelector('#wp'); // 初始化时传入name this.updateName(name); } updateName(name) { // 对外提供更新的api this.name = name; // 更新dom this.$div.innerHTML = name; } } class Parent { constructor() { // 初始化阶段 this.child = new Child('yan'); setTimeout(() => { // 实例化阶段 this.child.updateName('hou'); }, 2000); } }
在React中将两个状况统一处理,所有经过属性来完成,之因此可以这样是由于React在属性更新时会自动从新渲染子组件,
下面的例子中,2秒后子组件会自动从新渲染,并获取新的属性值java
class Child extends Component { render() { return <div>{this.props.name}</div> } } class Parent extends Component { constructor() { // 初始化阶段 this.state = {name: 'yan'}; setTimeout(() => { // 实例化阶段 this.setState({name: 'hou'}) }, 2000); } render() { return <Child name={this.state.name} /> } }
下面来看一会儿组件如何向父组件通讯,传统作法有两种,一种是回调函数,另外一种是为子组件部署消息接口react
先来看回调函数的例子,回调函数的优势是很是简单,缺点就是必须在初始化的时候传入,而且不可撤回,而且只能传入一个函数git
class Child { constructor(cb) { // 调用父组件传入的回调函数,发送消息 setTimeout(() => { cb() }, 2000); } } class Parent { constructor() { // 初始化阶段,传入回调函数 this.child = new Child(function () { console.log('child update') }); } }
下面来看看消息接口方法,首先须要一个能够发布和订阅消息的基类,好比下面实现了一个简单的EventEimtter
,实际生产中能够直接使用别人写好的类库,好比@jsmini/event,子组件继承消息基类,就有了发布消息的能力,而后父组件订阅子组件的消息,便可实现子组件向父组件通讯的功能github
消息接口的优势就是能够随处订阅,而且能够屡次订阅,还能够取消订阅,缺点是略显麻烦,须要引入消息基类api
// 消息接口,订阅发布模式,相似绑定事件,触发事件 class EventEimtter { constructor() { this.eventMap = {}; } sub(name, cb) { const eventList = this.eventMap[name] = this.eventMap[name] || {}; eventList.push(cb); } pub(name, ...data) { (this.eventMap[name] || []).forEach(cb => cb(...data)); } } class Child extends EventEimtter { constructor() { super(); // 经过消息接口发布消息 setTimeout(() => { this.pub('update') }, 2000); } } class Parent { constructor() { // 初始化阶段,传入回调函数 this.child = new Child(); // 订阅子组件的消息 this.child.sub('update', function () { console.log('child update') }); } }
Backbone.js就同时支持回调函数和消息接口方式,但React中选择了比较简单的回调函数模式,下面来看一下React的例子前端工程师
class Child extends Component { constructor(props) { setTimeout(() => { this.props.cb() }, 2000); } render() { return <div></div> } } class Parent extends Component { render() { return <Child cb={() => {console.log('update')}} /> } }
父子组件其实能够算是爷孙组件的一种特例,这里的爷孙组件不光指爷爷和孙子,而是泛指祖先与后代组件通讯,可能隔着不少层级,咱们已经解决了父子组件通讯的问题,根据化归法,很容易得出爷孙组件的答案,那就是层层传递属性么,把爷孙组件通讯分解为多个父子组件通讯的问题框架
层层传递的优势是很是简单,用已有知识就能解决,问题是会浪费不少代码,很是繁琐,中间做为桥梁的组件会引入不少不属于本身的属性
在React中,经过context可让祖先组件直接把属性传递到后代组件,有点相似星际旅行中的虫洞同样,经过context这个特殊的桥梁,能够跨越任意层次向后代组件传递消息
怎么在须要通讯的组件之间开启这个虫洞呢?须要双向声明,也就是在祖先组件声明属性,并在后代组件上再次声明属性,而后在祖先组件上放上属性就能够了,就能够在后代组件读取属性了,下面看一个例子
import PropTypes from 'prop-types'; class Child extends Component { // 后代组件声明须要读取context上的数据 static contextTypes = { text: PropTypes.string } render() { // 经过this.context 读取context上的数据 return <div>{this.context.text}</div> } } class Ancestor extends Component { // 祖先组件声明须要放入context上的数据 static childContextTypes = { text: PropTypes.string } // 祖先组件往context放入数据 getChildContext() { return {text: 'yanhaijing'} } }
context的优势是能够省去层层传递的麻烦,而且经过双向声明控制了数据的可见性,对于层数不少时,不失为一种方案;但缺点也很明显,就像全局变量同样,若是不加节制很容易形成混乱,并且也容易出现重名覆盖的问题
我的的建议是对一些全部组件共享的只读信息能够采用context来传递,好比登陆的用户信息等
小贴士:React Router路由就是经过context来传递路由属性的
若是两个组件是兄弟关系,能够经过父组件做为桥梁,来让两个组件之间通讯,这其实就是主模块模式
下面的例子中,两个子组件经过父组件来实现显示数字同步的功能
class Parent extends Component { constructor() { this.onChange = function (num) { this.setState({num}) }.bind(this); } render() { return ( <div> <Child1 num={this.state.num} onChange={this.onChange}> <Child2 num={this.state.num} onChange={this.onChange}> </div> ); } }
主模块模式的优势就是解耦,把两个子组件之间的耦合关系,解耦成子组件和父组件之间的耦合,把分散的东西收集在一块儿好处很是明显,能带来更好的可维护性和可扩展性
任意组件包括上面的三种关系组件,上面三种关系应该优先使用上面介绍的方法,对于任意的两个组件间通讯,总共有三种办法,分别是共同祖先法,消息中间件和状态管理
基于咱们上面介绍的爷孙组件和兄弟组件,只要找到两个组件的共同祖先,就能够将任意组件之间的通讯,转化为任意组件和共同祖先之间的通讯,这个方法的好处就是很是简单,已知知识就能搞定,缺点就是上面两种模式缺点的叠加,除了临时方案,不建议使用这种方法
另外一种比较经常使用的方法是消息中间件,就是引入一个全局消息工具,两个组件经过这个全局工具进行通讯,这样两个组件间的通讯,就经过全局消息媒介完成了
还记得上面介绍的消息基类吗?下面的例子中,组件1和组件2经过全局event进行通讯
class EventEimtter { constructor() { this.eventMap = {}; } sub(name, cb) { const eventList = this.eventMap[name] = this.eventMap[name] || {}; eventList.push(cb); } pub(name, ...data) { (this.eventMap[name] || []).forEach(cb => cb(...data)); } } // 全局消息工具 const event = new EventEimtter; // 一个组件 class Element1 extends Component { constructor() { // 订阅消息 event.sub('element2update', () => {console.log('element2 update')}); } } // 另外一个组件。 class Element2 extends Component { constructor() { // 发布消息 setTimeout(function () { event.pub('element2update') }, 2000) } }
消息中间件的模式很是简单,利用了观察者模式,将两个组件之间的耦合解耦成了组件和消息中心+消息名称的耦合,但为了解耦却引入全局消息中心和消息名称,消息中心对组件的侵入性很强,和第三方组件通讯不能使用这种方式
小型项目比较适合使用这种方式,但随着项目规模的扩大,达到中等项目之后,消息名字爆炸式增加,消息名字的维护成了棘手的问题,重名几率极大,没有人敢随便删除消息信息,消息的发布者找不到消息订阅者的信息等
其实上面的问题也不是没有解决办法,重名的问题能够经过制定规范,消息命名空间等方式来极大下降冲突,其余问题能够经过把消息名字统一维护到一个文件,经过对消息的中心化管理,可让不少问题都很容易解决
若是你的项目很是大,上面两种方案都不合适,那你可能须要一个状态管理工具,经过状态管理工具把组件之间的关系,和关系的处理逻辑从组建中抽象出来,并集中化到统一的地方来处理,Redux就是一个很是不错的状态管理工具
除了Redux,还有Mobx,Rematch,reselect等工具,本文不展开介绍,有机会后面单独成文,这些都是用来解决不一样问题的,只要根据本身的场景选择合适的工具就行了
组件间的关系变幻无穷,均可以用上面介绍的方法解决,对于不一样规模的项目,应该选择适合本身的技术方案,上面介绍的不一样方式解耦的程度是不同的,关于不一样耦合关系的好坏,能够看我以前的文章《图解7种耦合关系》
本文节选自个人新书《React 状态管理与同构实战》,感兴趣的同窗能够继续阅读本书,这本书由我和前端自身技术侯策协力打磨,凝结了咱们在学习、实践 React 框架过程当中的积累和心得。除了 React 框架使用介绍之外,着重剖析了状态管理以及服务端渲染同构应用方面的内容。同时吸收了社区大量优秀思想,进行概括比对。
本书受到百度公司副总裁沈抖、百度高级前端工程师董睿,以及知名JavaScript语言专家阮一峰、Node.js布道者狼叔、Flarum中文社区创始人 justjavac、新浪移动前端技术专家小爝、知乎知名博主顾轶灵等前端圈众多专家大咖的联协力荐。
有兴趣的读者能够点击下面的连接购买,再次感谢各位的支持与鼓励!恳请各位批评指正!
京东:https://item.jd.com/12403508....