详解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 相关文章推荐
IE Firefox 使用自定义标签的区别
Oct 15 Javascript
基于jQuery的自动完成插件
Feb 03 Javascript
使用jQuery时Form表单元素ID和name命名大忌
Mar 06 Javascript
javascript的BOM汇总
Jul 16 Javascript
JS 调用微信扫一扫功能
Dec 22 Javascript
基于JavaScript实现带缩略图的轮播效果
Jan 12 Javascript
ES6新特性之变量和字符串用法示例
Apr 01 Javascript
React.js中常用的ES6写法总结(推荐)
May 09 Javascript
javascript实现遮罩层动态效果实例
May 14 Javascript
使用Taro实现小程序商城的购物车功能模块的实例代码
Jun 05 Javascript
uni-app使用countdown插件实现倒计时
Nov 01 Javascript
如何使用 vue-cli 创建模板项目
Nov 19 Vue.js
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
php准确计算复活节日期的方法
2015/04/18 PHP
thinkphp中字符截取函数msubstr()用法分析
2016/01/09 PHP
Javascript 学习书 推荐
2009/06/13 Javascript
使用Microsoft Ajax Minifier减小JavaScript文件大小的方法
2010/04/01 Javascript
html5的自定义data-*属性和jquery的data()方法的使用示例
2013/08/21 Javascript
JS Loading功能的简单实现
2013/11/29 Javascript
js强制把网址设为默认首页
2015/09/29 Javascript
JS动态加载脚本并执行回调操作
2016/08/24 Javascript
JS for...in 遍历语句用法实例分析
2016/08/24 Javascript
文件上传插件SWFUpload的使用指南
2016/11/29 Javascript
JavaScript的事件机制详解
2017/01/17 Javascript
jQuery按需加载轮播图(web前端性能优化)
2017/02/17 Javascript
Vue组件化通讯的实例代码
2017/06/23 Javascript
js实现rem自动匹配计算font-size的示例
2017/11/18 Javascript
通过jquery获取上传文件名称、类型和大小的实现代码
2018/04/19 jQuery
jQuery实现的监听导航滚动置顶状态功能示例
2018/07/23 jQuery
uni app仿微信顶部导航条功能
2019/09/17 Javascript
在vue-cli3中使用axios获取本地json操作
2020/07/30 Javascript
js利用iframe实现选项卡效果
2020/08/09 Javascript
[01:09:40]Newbee vs Pain 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
Python获取网页上图片下载地址的方法
2015/03/11 Python
在django中,关于session的通用设置方法
2019/08/06 Python
对python pandas中 inplace 参数的理解
2020/06/27 Python
Python实现哲学家就餐问题实例代码
2020/11/09 Python
python用分数表示矩阵的方法实例
2021/01/11 Python
关于HTML5语义标签的实践(blog页面)
2016/07/12 HTML / CSS
简述Html5 IphoneX 适配方法
2018/02/08 HTML / CSS
Darphin迪梵官网: 来自巴黎,植物和精油调制的护肤品牌
2016/10/11 全球购物
YSL圣罗兰美妆美国官网:Yves Saint Lauret US
2016/11/21 全球购物
联想韩国官网:Lenovo Korea
2018/05/10 全球购物
Moss Bros官网:英国排名第一的西装店
2020/02/26 全球购物
高中生学习生活的自我评价
2013/11/27 职场文书
小班开学寄语
2014/04/04 职场文书
2015年大班保育员工作总结
2015/05/18 职场文书
法人代表证明书范本
2015/06/18 职场文书
Python中的协程(Coroutine)操作模块(greenlet、gevent)
2022/05/30 Python