详解React中的组件通信问题


Posted in Javascript onJuly 31, 2017

引入

本来我是没想过总结这些东西的,会感觉比较入门。但是之前同学去腾讯面试问到了这个问题(react或vue的组件通信),我帮他整理,顺便写demo的过程中,会有一些新的体会,多总结还是有利于进步的呀。

父子组件

父 → 子

parent组件传给child组件,符合react的单向数据流理念,自上到下传递props。

// 父组件
class Parent extends Component {
 constructor() {
  super();
  this.state = {
   value: '',
  }
 }

 handleChange = e => {
  this.value = e.target.value;
 }

 handleClick = () => {
  this.setState({
   value: this.value,
  })
 }

 render() {
  return (
   <div>
    我是parent
    <input onChange={this.handleChange} />
    <div className="button" onClick={this.handleClick}>通知</div>
    <div>
     <Child value={this.state.value} />
    </div> 
   </div>
  );
 }
}
// 子组件
class Child extends Component {
 render() {
  const { value } = this.props;
  return (
   <div>
    我是Child,得到传下来的值:{value}
   </div>
  );
 }
}

父组件做的就是定义好 state ,定义好事件函数,input onChange 的时候,去缓存 value 值,然后点击 button 的时候,改变 state , 子组件只负责展示 value 。

子 → 父

child 组件通知 parent 组件, 主要是依靠 parent 传下来的 callback 函数执行,改变 parent 组件的状态,或者把 child 自己的 state 通知 parent 。分两种情况:

state 定义在 parent 组件

// parent

class Parent extends Component {
 constructor() {
  super();
  this.state = {
   value: '',
  }
 }

 setValue = value => {
  this.setState({
   value,
  })
 }

 render() {
  return (
   <div>
    <div>我是parent, Value是:{this.state.value}</div> 
    <Child setValue={this.setValue} />
   </div>
  );
 }
}
class Child extends Component {

 handleChange = e => {
  this.value = e.target.value;
 }

 handleClick = () => {
  const { setValue } = this.props;
  setValue(this.value);
 }

 render() {
  return (
   <div>
    我是Child
    <div className="card">
     state 定义在 parent
     <input onChange={this.handleChange} />
     <div className="button" onClick={this.handleClick}>通知</div>
    </div>
   </div>
  );
 }
}

parent 组件把改变 state 的 setValue 函数传给 child ,child 组件自己处理内部的状态(这里是表单的value值),当 child 组件分发消息的时候, 执行 parent 的 setValue 函数,从而改变了 parent 的 state,state发生变化, parent 组件执行 re-render 。

state 定义在 child 组件

// parent

class Parent extends Component {

 onChange = value => {
  console.log(value, '来自 child 的 value 变化');
 }

 render() {
  return (
   <div>
    <div>我是parent
    <Child onChange={this.onChange} />
   </div>
  );
 }
}
class Child extends Component {

 constructor() {
  super();
  this.state = {
   childValue: ''
  }
 }

 childValChange = e => {
  this.childVal = e.target.value;
 }

 childValDispatch = () => {
  const { onChange } = this.props;
  this.setState({
   childValue: this.childVal,
  }, () => { onChange(this.state.childValue) })
 }

 render() {
  return (
   <div>
    我是Child
    <div className="card">
     state 定义在 child
     <input onChange={this.childValChange} />
     <div className="button" onClick={this.childValDispatch}>通知</div>
    </div>
   </div>
  );
 }
}

有时候 state 是需要定义在 child 组件的,比如弹窗, CheckBox 这种开关性质的,逻辑是重复的,state 定义在组件内部更好维护, 复用性更好。但是 child 的 state 是需要告知我的 parent 组件的, 同样还是执行 parent 传下来的 change 函数。

兄弟组件

有时候可能出现页面中的某两部分通信,比如省市的级联选择,点击 button 改变颜色等等,组件并不是父子级,没有嵌套关系的时候。这种时候通常是依赖共有的顶级 Container 处理或者第三方的状态管理器。其实原理都是相通的,兄弟 A 的 value 发生变化,分发的时候把 value 值告诉一个中间者 C ,C 会自动告知 B,实现 B 的自动render 。

利用共有的Container

// container
class Container extends Component {
 constructor() {
  super();
  this.state = {
   value: '',
  }
 }

 setValue = value => {
  this.setState({
   value,
  })
 }

 render() {
  return (
   <div>
    <A setValue={this.setValue}/>
    <B value={this.state.value} />
   </div>
  );
 }
}
// 兄弟A
class A extends Component {

 handleChange = (e) => {
  this.value = e.target.value;
 }

 handleClick = () => {
  const { setValue } = this.props;
  setValue(this.value);
 }

 render() {
  return (
   <div className="card">
    我是Brother A, <input onChange={this.handleChange} />
    <div className="button" onClick={this.handleClick}>通知</div>
   </div>
  )
 }
}
// 兄弟B
const B = props => (
 <div className="card">
  我是Brother B, value是:
  {props.value}
 </div>
);
export default B;

组件 A 中的表单 value 值,告知了父级 Container 组件(通过 setValue 函数改变 state),组件 B 依赖于 Container 传下来的 state,会做出同步更新。这里的中间者是 Container。

利用Context

上面的方式,如果嵌套少还可以,如果嵌套特别多,比如一级导航栏下的二级导航栏下的某个按钮,要改变页面中 content 区域的 table 里的某个列的值...他们同属于一个 page 。这样传递 props 就会很痛苦,每一层组件都要传递一次。

// 顶级公共组件
class Context extends Component {

 

constructor() {
  super();
  this.state = {
   value: '',
  };
 }

 setValue = value => {
  this.setState({
   value,
  })
 }

 getChildContext() { // 必需
  return { 
   value: this.state.value,
   setValue: this.setValue,
  };
 }
 render() {
  return (
   <div>
    <AParent />
    <BParent />
   </div>
  );
 }
}
// 必需
Context.childContextTypes = {
 value: PropTypes.string,
 setValue: PropTypes.func,
};
// A 的 parent
class AParent extends Component {
 render() {
  return (
   <div className="card">
    <A />
   </div>
  );
 }
}
// A
class A extends Component {

 handleChange = (e) => {
  this.value = e.target.value;
 }

 handleClick = () => {
  const { setValue } = this.context;
  setValue(this.value);
 }

 render() {
  return (
   <div>
    我是parentA 下的 A, <input onChange={this.handleChange} />
    <div className="button" onClick={this.handleClick}>通知</div>
   </div>
  );
 }
}
// 必需
A.contextTypes = {
 setValue: PropTypes.func,
};
// B 的 parent
class BParent extends Component {
 render() {
  return (
   <div className="card">
    <B />
   </div>
  );
 }
}

// B
class B extends Component {

 render() {
  return (
   <div>
    我是parentB 下的 B, value是:
    {this.context.value}
   </div>
  );
 }
}

B.contextTypes = {
 value: PropTypes.string,
};

组件 A 仍是 消息的发送者,组件 B 是接收者, 中间者是 Context 公有 Container 组件。context是官方文档的一个 API ,通过 getChildContext 函数定义 context 中的值,并且还要求 childContextTypes 是必需的。这样属于这个 Container 组件的子组件,通过 this.context 就可以取到定义的值,并且起到跟 state 同样的效果。中间者其实还是 Container,只不过利用了上下文这样的 API ,省去了 props 的传递。另外:这个功能是实验性的,未来可能会有所改动。

发布订阅

这种一个地方发送消息,另一个地方接收做出变化的需求,很容易想到的就是观察者模式了。具体的实现会有很多种,这里我们自己写了一个 EventEmitter 的类(其实就是仿照 node 中的 EventEmitter 类),如果不了解观察者,可以看我的另一篇文章 观察者模式 。

// 发布订阅类
class EventEmitter {
 _event = {}

 // on 函数用于绑定
 on(eventName, handle) {
  let listeners = this._event[eventName];
  if(!listeners || !listeners.length) {
   this._event[eventName] = [handle];
   return;
  }
  listeners.push(handle);
 }
 // off 用于移除
 off(eventName, handle) {
  let listeners = this._event[eventName];
  this._event[eventName] = listeners.filter(l => l !== handle);
 }
 // emit 用于分发消息
 emit(eventName, ...args) {
  const listeners = this._event[eventName];
  if(listeners && listeners.length) {
   for(const l of listeners) {
    l(...args);
   }
  }
 }
}
const event = new EventEmitter;
export { event };
// Container
import A from './a';
import B from './b';

const Listener = () => {
 return (
  <div>
   <A />
   <B />
  </div>
 );
};
export default Listener;
// 兄弟组件 A
import { event } from './eventEmitter';

class A extends Component {

 handleChange = e => {
  this.value = e.target.value;
 }

 handleClick = () => {
  event.emit('dispatch', this.value);
 }

 render() {
  return (
   <div className="card">
    我是Brother A, <input onChange={this.handleChange} />
    <div className="button" onClick={this.handleClick}>通知</div>
   </div>
  )
 }
}
// 兄弟组件 B
import { event } from './eventEmitter';

class B extends Component {
 state = {
  value: ''
 }

 componentDidMount() {
  event.on('dispatch', this.valueChange);
 }

 componentWillUnmount() {
  event.off('dispatch', this.valueChange);
 }

 valueChange = value => {
  this.setState({
   value,
  })
 }

 render() {
  return (
   <div className="card">
    我是Brother B, value是:
    {this.state.value}
   </div>
  );
 }
}

仍然是组件 A 用于分发消息,组件 B 去接收消息。这里的中间者其实就是 event 对象。需要接收消息的 B 去订阅 dispatch 事件,并把回调函数 valueChange 传入,另外 B 定义了自己的 state,方便得到 value 值的时候自动渲染。组件 A 其实就是把内部的表单 value 在点击的时候分发,发布事件,从而 B 中的 valueChange 执行,改变 state。这种方式比较方便,也更直观,不需要借助 Container 组件去实现,省去了很多逻辑。

Redux || Mobx

Redux 或者 Mobx 是第三方的状态管理器,是这里我们通信的中间者。大型项目最直接的就是上库... 更方便,更不容易出错。 但其实小项目就没什么必要了。东西比较多,这里不再阐述它们的实现和做了什么。

总结

react 特殊的自上而下的单向数据流,和 state 的特性,造就以这样的思想实现组件通信。除去发布订阅和 Redux 等,其他的都是 props 自上而下传递的理念,子组件需要的总是通过父组件传递下来的,关于 state 的定义,还是看具体的应用场景了。

另外本次的代码都放在https://github.com/sunyongjian/rc-communication-demo, 可以 done 下来加深理解。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
运用jquery实现table单双行不同显示并能单行选中
Jul 25 Javascript
通过继承IHttpHandle实现JS插件的组织与管理
Jul 13 Javascript
js日期时间补零的小例子
Mar 05 Javascript
图片Slider 带左右按钮的js示例
Aug 30 Javascript
jquery showModelDialog的使用方法示例详解
Nov 19 Javascript
Js冒泡事件详解及阻止示例
Mar 21 Javascript
JavaScript条件判断_动力节点Java学院整理
Jun 26 Javascript
Textarea输入字数限制实例(兼容iOS&amp;安卓)
Jul 06 Javascript
元素全屏的设置与监听实例
Nov 28 Javascript
vue引入ueditor及node后台配置详解
Jan 03 Javascript
vue和webpack安装命令详解
Jun 15 Javascript
JavaScript实现简单图片切换
Apr 29 Javascript
Angular.js前台传list数组由后台spring MVC接收数组示例代码
Jul 31 #Javascript
Angular.js中数组操作的方法教程
Jul 31 #Javascript
BootStrap导航栏问题记录
Jul 31 #Javascript
Angular4 中内置指令的基本用法
Jul 31 #Javascript
详谈ES6中的迭代器(Iterator)和生成器(Generator)
Jul 31 #Javascript
浅谈对Angular中的生命周期钩子的理解
Jul 31 #Javascript
Bootstrap Table 在指定列中添加下拉框控件并获取所选值
Jul 31 #Javascript
You might like
关于文本留言本的分页代码
2006/10/09 PHP
PHP备份数据库生成SQL文件并下载的函数代码
2012/02/05 PHP
php编写的简单页面跳转功能实现代码
2013/11/27 PHP
使用swoole扩展php websocket示例
2014/02/13 PHP
php将access数据库转换到mysql数据库的方法
2014/12/24 PHP
Laravel5.* 打印出执行的sql语句的方法
2017/07/24 PHP
laravel 验证错误信息到 blade模板的方法
2019/09/29 PHP
javascript 延迟加载技术(lazyload)简单实现
2011/01/17 Javascript
js 实现 input type=&quot;file&quot; 文件上传示例代码
2013/08/07 Javascript
JavaScript使用Max函数返回两个数字中较大数的方法
2015/04/06 Javascript
javascript中checkbox使用方法实例演示
2015/11/19 Javascript
AngularJS中处理多个promise的方式
2016/02/02 Javascript
JavaScript中的遍历详解(多种遍历)
2017/04/07 Javascript
详解vue-router基本使用
2017/04/18 Javascript
js实现日期显示的一些操作(实例讲解)
2017/07/27 Javascript
谈谈对vue响应式数据更新的误解
2017/08/01 Javascript
hammer.js实现图片手势放大效果
2017/08/29 Javascript
微信小程序项目实践之九宫格实现及item跳转功能
2018/07/19 Javascript
优雅的在React项目中使用Redux的方法
2018/11/10 Javascript
javascript canvas时钟模拟器
2020/07/13 Javascript
JS使用setInterval计时器实现挑战10秒
2020/11/08 Javascript
Python中几种操作字符串的方法的介绍
2015/04/09 Python
Python进程间通信用法实例
2015/06/04 Python
基于python OpenCV实现动态人脸检测
2018/05/25 Python
详解python破解zip文件密码的方法
2020/01/13 Python
Keras SGD 随机梯度下降优化器参数设置方式
2020/06/19 Python
CSS3 please 跨浏览器的CSS3产生器
2010/03/14 HTML / CSS
学校纪律作风整改措施思想汇报
2014/10/11 职场文书
2015政治思想表现评语
2015/03/25 职场文书
廉政党课工作报告案例
2019/06/21 职场文书
中秋节英文祝福语句(14句)
2019/09/11 职场文书
基于python制作简易版学生信息管理系统
2021/04/20 Python
Django migrate报错的解决方案
2021/05/20 Python
Pytorch可视化的几种实现方法
2021/06/10 Python
Ruby使用Mysql2连接操作MySQL
2022/04/19 Ruby
Mybatis-Plus 使用 @TableField 自动填充日期
2022/04/26 Java/Android