详解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实现的模拟QQ邮箱里的收件人选取及其他效果(一)
Jan 06 Javascript
父元素与子iframe相互获取变量和元素对象的具体实现
Oct 15 Javascript
javaScript使用EL表达式的几种方式
May 27 Javascript
jquery实现点击页面计算点击次数
Jan 23 Javascript
jquery采用oop模式class类的使用示例
Jan 22 Javascript
JS组件Bootstrap dropdown组件扩展hover事件
Apr 17 Javascript
浅谈html转义及防止javascript注入攻击的方法
Dec 04 Javascript
jQuery实现标签页效果实战(4)
Feb 08 Javascript
React复制到剪贴板的示例代码
Aug 22 Javascript
在vscode里使用.vue代码模板的方法
Apr 28 Javascript
JS实现百度网盘任意文件强制下载功能
Aug 31 Javascript
使用Vue3+Vant组件实现App搜索历史记录功能(示例代码)
Jun 09 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
《星际争霸II》全新指挥官斯台特曼现已上线
2020/03/08 星际争霸
php jq jquery getJSON跨域提交数据完整版
2013/09/13 PHP
PHP设计模式入门之状态模式原理与实现方法分析
2020/04/26 PHP
Textarea与懒惰渲染实现代码
2012/01/04 Javascript
JavaScript调用堆栈及setTimeout使用方法深入剖析
2013/02/16 Javascript
javascript中call和apply的用法示例分析
2015/04/02 Javascript
详解VueJS 数据驱动和依赖追踪分析
2017/07/26 Javascript
js实现移动端轮播图效果
2020/12/09 Javascript
在页面中引入js的两种方法(推荐)
2017/08/29 Javascript
JS继承与闭包及JS实现继承的三种方式
2017/10/15 Javascript
JavaScript变量声明var,let.const及区别浅析
2018/04/23 Javascript
Vue导出页面为PDF格式的实现思路
2018/07/31 Javascript
浅谈express.js框架中间件(middleware)
2019/04/07 Javascript
vue-mugen-scroll组件实现pc端滚动刷新
2019/08/16 Javascript
vue移动端实现手机左右滑动入场动画
2020/06/17 Javascript
[05:29]2014DOTA2国际邀请赛 赛后专访:LGDNewbee顺利过关
2014/07/13 DOTA
[01:51]DAC趣味视频-如何成为职业选手.mp4
2017/04/02 DOTA
Django框架中处理URLconf中特定的URL的方法
2015/07/20 Python
python通过文件头判断文件类型
2015/10/30 Python
python+opencv识别图片中的圆形
2020/03/25 Python
python使用turtle库绘制时钟
2020/03/25 Python
Pycharm无法显示动态图片的解决方法
2018/10/28 Python
django 微信网页授权登陆的实现
2019/07/30 Python
Django项目主urls导入应用中views的红线问题解决
2019/08/10 Python
Python-Flask:动态创建表的示例详解
2019/11/22 Python
在Python中预先初始化列表内容和长度的实现
2019/11/28 Python
使用python实现微信小程序自动签到功能
2020/04/27 Python
Tensorflow与Keras自适应使用显存方式
2020/06/22 Python
虚拟机下载python是否需要联网
2020/07/27 Python
英国鲜花速递:Serenata Flowers
2018/04/03 全球购物
Cynthia Rowley官网:全球领先的生活方式品牌
2020/10/27 全球购物
俄语专业毕业生推荐信
2013/10/28 职场文书
优秀毕业生自荐信
2014/06/10 职场文书
工作作风承诺书
2014/08/30 职场文书
十岁生日答谢词
2015/01/05 职场文书
分析ZooKeeper分布式锁的实现
2021/06/30 Java/Android