根据组件之间的嵌套关系(即层级关系)可分为4种通信方式:父子、兄弟、跨级和无级。

一、父子通信

  在React中,数据是自顶向下单向流动的,而父组件通过props向子组件传递需要的信息是组件之间最常见的通信方式,如下代码所示,父组件Parent向子组件Child传递了一个name属性,其值为一段字符串“strick”。

  1. class Parent extends React.Component {
  2. render() {
  3. return <Child name="strick">子组件</Child>;
  4. }
  5. }
  6. class Child extends React.Component {
  7. render() {
  8. return <input name={this.props.name} type="text" />;
  9. }
  10. }

  当需要子组件向父组件传递信息时,也能通过组件的props实现,只是要多传一个回调函数,如下所示。

  1. class Parent extends React.Component {
  2. callback(value) {
  3. console.log(value); //输出从子组件传递过来的值
  4. }
  5. render() {
  6. return <Child callback={this.callback} />;
  7. }
  8. }
  9. class Child extends React.Component {
  10. constructor(props) {
  11. super(props);
  12. this.state = { name: "" };
  13. }
  14. handle(e) {
  15. this.props.callback(e.target.value); //调用父组件的回调函数
  16. this.setState({ name: e.target.value }); //更新文本框中的值
  17. }
  18. render() {
  19. return <input value={this.state.name} type="text" onChange={this.handle.bind(this)} />;
  20. }
  21. }

  父组件Parent会传给子组件Child一个callback()方法,子组件中的文本框注册了一个onChange事件,在事件处理程序handle()中将回调父组件的callback()方法,并把文本框的值传递过去,以此达到反向通信的效果。

二、兄弟通信

  当两个组件拥有共同的父组件时,就称它们为兄弟组件,注意,它们可以不在一个层级上,如图6所示,C与D或E都是兄弟关系。

图6  组件树

  兄弟之间不能直接通信,需要借助状态提升的方式间接实现信息的传递,即把组件之间要共享的状态提升至最近的父组件中,由父组件来统一管理。而任意一个兄弟组件可通过从父组件传来的回调函数更新共享状态,新的共享状态再通过父组件的props回传给子组件,从而完成一次兄弟之间的通信。在下面的例子中,会有两个文本框(如图7所示),当向其中一个输入数字时,邻近的文本框会随之改变,要么加一,要么减一。

图7  两个文本框

  1. class Parent extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { type: "p", digit: 0 };
  5. this.plus = this.plus.bind(this);
  6. this.minus = this.minus.bind(this);
  7. }
  8. plus(digit) {
  9. this.setState({ type: "p", digit });
  10. }
  11. minus(digit) {
  12. this.setState({ type: "m", digit });
  13. }
  14. render() {
  15. let { type, digit } = this.state;
  16. let pdigit = type == "p" ? digit : (digit+1);
  17. let mdigit = type == "m" ? digit : (digit-1);
  18. return (
  19. <>
  20. <Child type="p" digit={pdigit} onDigitChange={this.plus} />
  21. <Child type="m" digit={mdigit} onDigitChange={this.minus} />
  22. </>
  23. );
  24. }
  25. }
  26. class Child extends React.Component {
  27. constructor(props) {
  28. super(props);
  29. this.handle = this.handle.bind(this);
  30. }
  31. handle(e) {
  32. this.props.onDigitChange(+e.target.value);
  33. }
  34. render() {
  35. return (
  36. <input value={this.props.digit} type="text" onChange={this.handle} />
  37. );
  38. }
  39. }

  上面代码实现了一次完整的兄弟之间的通信,具体过程如下所列。

(1)首先在父组件Parent中定义两个兄弟组件Child,其中type属性为“p”的子组件用于递增,绑定了plus()方法;type属性为“m”的子组件用于递减,绑定了minus()方法。

(2)然后在子组件Child中接收传递过来的digit属性和onDigitChange()方法,前者会作为文本框的值,后者会在事件处理程序onChange()中被调用。

(3)如果在递增文本框中修改数值,那么就将新值传给plus()方法。递减文本框的处理过程与之类似,只是将plus()方法替换成minus()方法。

(4)最后更新父组件中的两个状态:type和digit,完成信息的传递。

三、跨级通信

  在一棵组件树中,当多个组件需要跨级通信时,所处的层级越深,那么需要过渡的中间层就越多,完成一次通信将变得非常繁琐,而在数据传递过程中那些作为桥梁的组件,其代码也将变得冗余且臃肿。

  在React中,还可用Context实现跨级通信。Context能存放组件树中需要全局共享的数据,也就是说,一个组件可以借助Context跨越层级直接将数据传递给它的后代组件。如图8所示,左边的数据会通过组件的props逐级显式地传递,右边的数据会通过Context让所有组件都可访问。

图8  props和context

  随着React v16.3的发布,引入了一种全新的Context,修正了旧版本中较为棘手的问题,接下来的篇幅将着重分析这两个版本的Context。

1)旧的Context

  在旧版本的Context中,首先要在顶层组件内添加getChildContext()方法和静态属性childContextTypes,前者用于生成一个context对象(即初始化Context需要携带的数据),后者通过prop-types库限制该对象的属性的数据类型,两者缺一不可。在下面的示例中,Grandpa是顶层组件,Son是中间组件,要传递的是一个包含name属性的对象。

  1. //顶层组件
  2. class Grandpa extends React.Component {
  3. getChildContext() {
  4. return { name: "strick" };
  5. }
  6. render() {
  7. return <Son />;
  8. }
  9. }
  10. Grandpa.childContextTypes = {
  11. name: PropTypes.string
  12. };
  13. //中间组件
  14. class Son extends React.Component {
  15. render() {
  16. return <Grandson />;
  17. }
  18. }

  然后给后代组件(例如下面的Grandson)添加静态属性contextTypes,限制要接收的属性的数据类型,最后就能通过读取this.context得到由顶层组件提供的数据。

  1. class Grandson extends React.Component {
  2. render() {
  3. return <p>{this.context.name}</p>;
  4. }
  5. }
  6. Grandson.contextTypes = {
  7. name: PropTypes.string
  8. };

  从上面的示例中可以看出,跨级通信的准备工作并不简单,需要在两处做不同的配置。React官方建议慎用旧版的Context,因为它相当于JavaScript中的全局变量,容易造成数据流混乱、重名覆盖等各种副作用,并且在未来的React版本中有可能被废弃。

  虽然在功能上Context实现了跨级通信,但本质上数据还是像props一样逐级传递的,因此如果某个中间组件的shouldComponentUpdate()方法返回false的话,就会阻止下层的组件更新Context中的数据。接下来会演示这个致命的缺陷,沿用上一个示例,对两个组件做些调整。在Grandpa组件中,先让Context保存组件的name状态,再新增一个按钮,并为其注册一个能更新组件状态的点击事件;在Son组件中,添加shouldComponentUpdate()方法,它的返回值是false。在把Grandpa组件挂载到DOM中后,点击按钮就能发现Context的更新传播终止于Son组件。

  1. class Grandpa extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { name: "strick" };
  5. this.click = this.click.bind(this);
  6. }
  7. getChildContext() {
  8. return { name: this.state.name };
  9. }
  10. click() {
  11. this.setState({ name: "freedom" });
  12. }
  13. render() {
  14. return (
  15. <>
  16. <Son />
  17. <button onClick={this.click}>提交</button>
  18. </>
  19. );
  20. }
  21. }
  22. class Son extends React.Component {
  23. shouldComponentUpdate() {
  24. return false;
  25. }
  26. render() {
  27. return <Grandson />;
  28. }
  29. }

2)新的Context

  这个版本的Context不仅采用了更符合React风格的声明式写法,还可以直接将数据传递给后代组件而不用逐级传递,一举冲破了shouldComponentUpdate()方法的限制。下面仍然使用上一节的三个组件,完成一次新的跨级通信。

  1. const NameContext = React.createContext({name: "strick"});
  2. class Grandpa extends React.Component {
  3. render() {
  4. return (
  5. <NameContext.Provider value={{name: "freedom"}}>
  6. <Son />
  7. </NameContext.Provider>
  8. );
  9. }
  10. }
  11. class Son extends React.Component {
  12. render() {
  13. return <Grandson />;
  14. }
  15. }
  16. class Grandson extends React.Component {
  17. render() {
  18. return (
  19. <NameContext.Consumer>{context => <p>{context.name}</p>}</NameContext.Consumer>
  20. );
  21. }
  22. }

  通过上述代码可知,新的Context由三部分组成:

(1)React.createContext()方法,接收一个可选的defaultValue参数,返回一个Context对象(例如NameContext),包含两个属性:Provider和Consumer,它们是一对相呼应的组件。

(2)Provider,来源组件,它的value属性就是要传送的数据,Provider可关联多个来自于同一个Context对象的Consumer,像NameContext.Provider只能与NameContext.Consumer配合使用。

(3)Consumer,目标组件,出现在Provider之后,可接收一个返回React元素的函数,如果Consumer能找到对应的Provider,那么函数的参数就是Provider的value属性,否则就读取defaultValue的值。

  注意,Provider组件会通过Object.is()对其value属性的新旧值做比较,以此确定是否更新作为它后代的Consumer组件。

四、无级通信

  当两个没有嵌套关系(即无级)的组件需要通信时,可以借助消息队列实现。下面是一个用观察者模式实现的简易消息队列库,其处理过程类似于事件系统,如果将消息看成事件,那么订阅消息就是绑定事件,而发布消息就是触发事件。

  1. class EventEmitter {
  2. constructor() {
  3. this.events = {};
  4. }
  5. sub(event, listener) { //订阅消息
  6. if (!this.events[event]) {
  7. this.events[event] = { listeners: [] };
  8. }
  9. this.events[event].listeners.push(listener);
  10. }
  11. pub(name, ...params) { //发布消息
  12. for (const listener of this.events[name].listeners) {
  13. listener.apply(this, params);
  14. }
  15. }
  16. }

  EventEmitter只包含了三个方法,它们的功能如下所列:

(1)构造函数,初始化了一个用于缓存各类消息的容器。

(2)sub()方法,将回调函数用消息名称分类保存。

(3)pub()方法,依次执行了指定名称下的消息集合。

  下面用一个示例演示无级通信,在Sub组件的构造函数中,会订阅一次消息,消息名称为"TextBox",回调函数会接收一个参数,并将其输出到控制台。

  1. let emitter = new EventEmitter();
  2. class Sub extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. emitter.sub("TextBox", value => console.log(value));
  6. }
  7. render() {
  8. return <p>订阅消息</p>;
  9. }
  10. }

  在下面的Pub组件中,为文本框注册了onChange事件,在事件处理程序handle()中发布名为"TextBox"的消息集合,并将文本框中的值作为参数传递到回调函数中。

  1. class Pub extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = { value: "" };
  5. }
  6. handle(e) {
  7. const value = e.target.value;
  8. emitter.pub("TextBox", value);
  9. this.setState({ value });
  10. }
  11. render() {
  12. return <input value={this.state.value} onChange={this.handle.bind(this)} />;
  13. }
  14. }

  Sub组件和Pub组件会像下面这样,以兄弟的关系挂载到DOM中。当修改文本框中的内容时,就会触发消息的发布,从而完成了一次它们之间的通信。

  1. ReactDOM.render(
  2. <>
  3. <Sub />
  4. <Pub />
  5. </>,
  6. document.getElementById("container")
  7. );

  当业务逻辑复杂到一定程度时,普通的消息队列可能就捉襟见肘了,此时可以考虑引入Mobx、Redux等专门的状态管理工具来实现组件之间的通信。

React躬行记(9)——组件通信的更多相关文章

  1. React躬行记(7)——表单

    表单元素是一类拥有内部状态的元素,这些状态由其自身维护,通过这类元素可让用户与Web应用进行交互.HTML中的表单元素(例如<input>.<select>和<radio ...

  2. React躬行记(3)——组件

    组件(Component)由若干个React元素组成,包含属性.状态和生命周期等部分,满足独立.可复用.高内聚和低耦合等设计原则,每个React应用程序都是由一个个的组件搭建而成,即组成React应用 ...

  3. React躬行记(10)——高阶组件

    高阶组件(High Order Component,简称HOC)不是一个真的组件,而是一个没有副作用的纯函数,以组件作为参数,返回一个功能增强的新组件,在很多第三方库(例如Redux.Relay等)中 ...

  4. React躬行记(5)——React和DOM

    React实现了一套与浏览器无关的DOM系统,包括元素渲染.节点查询.事件处理等机制. 一.ReactDOM 自React v0.14开始,官方将与DOM相关的操作从React中剥离,组成单独的rea ...

  5. React躬行记(8)——样式

    由于React推崇组件模式,因此会要求HTML.CSS和JavaScript混合在一起,虽然这与过去的关注点分离正好相反,但是更有利于组件之间的隔离.React已将HTML用JSX封装,而对CSS只进 ...

  6. React躬行记(13)——React Router

    在网络工程中,路由能保证信息从源地址传输到正确地目的地址,避免在互联网中迷失方向.而前端应用中的路由,其功能与之类似,也是保证信息的准确性,只不过来源变成URL,目的地变成HTML页面. 在传统的前端 ...

  7. React躬行记(2)——JSX

    JSX既不是字符串,也不是HTML,而是一种类似XML,用于描述用户界面的JavaScript扩展语法,如下代码所示.在使用JSX时,为了避免自动插入分号时出现问题,推荐在其最外层用圆括号包裹,并且必 ...

  8. React躬行记(4)——生命周期

    组件的生命周期(Life Cycle)包含三个阶段:挂载(Mounting).更新(Updating)和卸载(Unmounting),在每个阶段都会有相应的回调方法(也叫钩子)可供选择,从而能更好的控 ...

  9. React躬行记(6)——事件

    React在原生事件的基础上,重新设计了一套跨浏览器的合成事件(SyntheticEvent),在事件传播.注册方式.事件对象等多个方面都做了特别的处理. 一.注册事件 合成事件采用声明式的注册方式, ...

随机推荐

  1. Android零基础入门第64节:揭开RecyclerView庐山真面目

    原文:Android零基础入门第64节:揭开RecyclerView庐山真面目 大家还记得之前在第38期~第50期都在学习列表控件吗,其中用了8期讲ListView的使用,相信都已经掌握好了吧.那么本 ...

  2. SVN更新报错问题(Please execute the 'Cleanup' command)

    SVN更新报错问题(Please execute the 'Cleanup' command) https://segmentfault.com/a/1190000012571289 svn: E20 ...

  3. Uncaught (in promise)

    Uncaught (in promise) 使用es6的promise时候,有时候会出现如下错误: 这是因为,使用定义promise方法的时候,reject了,但是,在使用的地方没有用catch进行接 ...

  4. 我的第一个Chrome插件:天气预报应用

    1.Chrome插件开发基础   开发Chrome插件很简单,只要会基本的前台技术HTML.CSS.JS就可以开发了. Chrome插件一般包括两个HTML页面background和popup.   ...

  5. SYN1621型 定位定向授时设备

    SYN1621型 定位定向授时设备 定位定向授时设备使用说明视频链接: http://www.syn029.com/h-pd-274-0_310_39_-1.html 请将此链接复制到浏览器打开观看 ...

  6. 一道经典的js面试题

    # 声明:学习编程语言最好的方式就是通过实例学习 ## 下面是我在博客上看到的一道js面试题,可以说非常经典,下面会以最简单的方式让你理解题目:```bashfunction Foo() { getN ...

  7. 分布式数据库中间件 MyCat 搞起来!

    关于 MyCat 的铺垫文章已经写了三篇了: MySQL 只能做小项目?松哥要说几句公道话! 北冥有 Data,其名为鲲,鲲之大,一个 MySQL 放不下! What?Tomcat 竟然也算中间件? ...

  8. Node 在chrome上调试

    先确保node是否安装成功: 然后 1.创建node_module块,并在同一层级创建一个服务: 如server.js: 2.下载调试工具devtool: npm install -g devtool ...

  9. vue 开发webapp 手机返回键 退出问题

    vue 开发webapp 手机返回键 退出问题 mui进行手机物理键的监听 首先安装 vue-awesome-mui npm i vue-awesome-mui 在main.js注册 在index.h ...

  10. Spring Bean的3种装配方式

    Bean常用的装配方式有3种: 基于xml的装配 基于Annotation(注解)的装配 基于Java类的装配 基于xml的装配 在xml文件中配置Bean. 如果依赖很多,xml配置文件会很臃肿,后 ...