react组件通讯通识篇

4,970 阅读7分钟

前言

本文为入门级别的技术文章以及读书笔记,大佬请忽略。demo地址:链接

父向子组件通讯(数据)

父组件向子组件通讯是我们开发中非常常见,其方式也一般是props属性最直接方便,当然这里指的是直接就是父子组件关系的。

这里只希望提示大家一点,如果你有需要,不要直接修改传入属性的值,如果你尝试直接修改会有提示不建议不应该这样操作,这个变量是只读的。如果你切实需要,你可以在state中接受并改变。

子向父组件通讯

通讯的方式主要有两种,分别是自定义事件以及回调函数。其中回调函数非常方便,而且可以拿到执行时的所有状态;其中自定义事件比较麻烦,我们一般使用比较少。

在下面的例子中,我使用的方式是回调函数。

// listItem.jsx
 render() {
    const { title } = this.state;
    const { click } = this.props;
    const title2 = title + "temp";
    return (
      <h2 onClick={click} onMouseDown={this.down}>
        {title2}
      </h2>
    );
  }

// list.jsx
render() {
    const { list } = this.state;
    const style = {
      height: 30
    };
    return (
      <div style={style}>
        {list.map(item => (
          <ListItem
            {...item}
            click={this.click.bind(this)}
            error={this.error}
          />
        ))}
      </div>
    );
  }

备注:如果你希望传递事件以外侧参数,你需要bind绑定传参或者写成简单的箭头函数。否则,直接写函数以及参数会编译成函数直接执行。(比如传参text,onClick = {text => this.click(text), 或者写成,onClick={this.click.bind(this,text)}).

兄弟组件通讯

兄弟组件如果是同一个父组件可以借助父组件进行通信,这里不多描述,这里分享的是基于事件发布订阅机制的实现,我们借助nodejs的Events模块。

根目录下创建一个单例的事件,然后导出,为了避免浪费,我们全局使用一个单例文件events.js。

import {EventEmitter} from 'events';
export default new EventEmitter();
// a组件中触发
import emitter from '../events';
emitter.emit('clickMsg','点击了')

// b组件监听
commpontentDidMount(){
	this.clickMsg = emitter.on('clickMsg',data => {
  	console.log(data)
  })
}
compontWillUnmount(){
	emitter.removeListener(this.clickMsg)
}

备注:事件的执行是否会多次执行?目前遇到的一个问题,日志会执行多次,文件中的说明,Hook a console constructor and forward messages to a callback 。

跨级组件通讯 -- context

跨级组件我们也可以认为是父子组件的叠加,因此可以使用多层父子组件的数据传递,但这样比较繁琐,因此更建议的方式是是通过context的方式,来实现。它的原理也比较简单,就是基于同一个父组件的前提下,它的任何层级的子组件都可以拿到这个父附件的状态,并以此进行通讯。

在之前的官网版本中,一直具有这个功能,不过不建议使用而已,但随着版本迭代,慢慢发现这个功能具有较大的便利性。

// 定义一个context的全局对象:需要注意的是你在provider的地方使用它的时候也必须符合基本的数据结构
import React from "react";
export const ThemeContext React.createContext({
  color: "black",
  label: "名字"
});
// 父或者顶层容器设计
import { ThemeContext} from "../themeContext";
	constructor(props){
    super(props);
    this.state = {
    	theme:{
        color:'pink',
        label:'你的名字'
      }
    }
  }
render(){
  return (
<ThemeContext.Provider value={this.state.theme}>
 </ThemeContext.Provider>
  )
}
// 任意层级的子组件
import {ThemeContext} from "../../themeContext";

render(){
			<ThemeContext.Consumer>
        {({ color, label }) => <label style={{ color: color }}>{label}</label>}
      </ThemeContext.Consumer>
} 
  • 动态context:我们可以直接在provider的时候动态的提供或者修改其默认值。
  • 使用建议:不要盲目使用context,仅当多个组件使用同一个公用数据时才建议引入context解决这个问题。
  • 特点: 提供一个全局或者任意组件可访问的context对象,然后组件通过修改或者消费这个context对象来实现使用。

context 与 redux的对比

  • 原先的传递方式是级联传递,新的方式是直接传递,效率更高。

image.png
 

  • 原先的方式通过级联的方式,如果某以及willUpdate中拦截返回false,那么其子都不能正常获取,新版的没有这个问题。

ant-design form组件中的使用

在antd 的form组件中,我们也看到对应的context的代码部分:文件地址:链接

// context 设置的默认值 
import createReactContext, { Context } from 'create-react-context';
import { ColProps } from '../grid/col';

export interface FormContextProps {
  vertical: boolean;
  colon?: boolean;
  labelAlign?: string;
  labelCol?: ColProps;
  wrapperCol?: ColProps;
}

export const FormContext: Context<FormContextProps> = createReactContext({
  labelAlign: 'right',
  vertical: false,
});
// 引入默认的context
import { FormContext } from './context';
// 获取传入的属性,然后解构,通过context provider的方式,state传值提供给包含在form组件中的组件  
render() {
    const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props;
    return (
      <FormContext.Provider
        value={{ wrapperCol, labelAlign, labelCol, vertical: layout === 'vertical', colon }}
      >
        <ConfigConsumer>{this.renderForm}</ConfigConsumer>
      </FormContext.Provider>
    );
  }

父组件调用子组件事件

虽然上面的方式提到了各种通讯机制,但对于父组件需要主动触发子组件的某个事件还是没有相应的方法。我们在下面的例子中给出这种需求的场景。比如:在容器组件中,我们需要主动刷新列表中的数据,而列表组件的数据是自己控制获取的。

备注:当然有的人会说,既然有如此的需求,为什么不把子组件的数据或者方法通过父组件的属性传入,把逻辑在父组件中维护,这样就没有这样的问题。固然是一种很不错的方法,但也确实存在有些时候,我们希望子组件更多的具有一些功能,而不重度依赖父组件,只是将自己的事件暴露给父组件。其实这种设计思路在vue中,在开源的ui框架中很多,称为自定义事件,我们除了属性传入控制组件之外,还可以通过组件的自定义事件,事件调用实现改变组件的机制。

方式一:refs 方式

class List extends React.Component{
  constructor(props){
    this.state ={
    desc:''
    }
  }
  xxxMethod(){
    const desc = Math.random();
  	this.setState({
    desc
    })
  }
 render(){
   const {list} = this.props;
 	return (
  <div>
      <button onClick={this.xxxMethod.bind(this)}>点击变动标题描述</button>
      {list.map(text =>(<h2>{text}{desc}</h2>))}
  </div>    
  )
 }
}

class parent extends React.Component{
refresh(){
 // 可以调用子组件的任意方法
this.refs.List.xxxMethod();
}
//父组件
render (){
  return (
    <div>
        <button onClick={this.refresh.bind(this)}>点击变动标题描述</button>
        <List ref="List" />
    </div>)
} 
}
    

方式二  通过主动调用返回this

通过回调事件传回this,然后赋值给父组件。看上去这段和ref的直接使用没有任何差别,不过能确定的是通过这种方式,我们能准确的拿到子组件的this实例。它为什么是合理存在的呢?因为有些情况下,我们通过ref拿到的不是我们想要的子组件中的属性或者方法。

class Child extends React.Component{
	constructor(props){
  	super(props);
  }
  componentDidMount(){
  	this.props.onRef(this)
  }
  console(){
  	console.log('子组件的方法');
  }
  render(){
  	return (<div>子组件</div>)
  }
}

class Parent extends React.Component{
  onRef(comp){
  	this.child = comp;
  }
  console(){
  	this.child.console()
  }
 render(){
  return (
  <div>
      <button onClick={this.console.bind(this)}>点击执行子组件方法</button>
      <Child onRef={this.onRef.bind(this)}/>
 </div>    
  )
 }
}

方式三:HOC

通过高阶组件的方式,我们可以一次性解决类似的需求,而不是每次都手动的写this的暴露方法。在你需要的位置引入,然后父子组件分别写上@withRef。

import React from "react";
export default WrappedComponent => {
  return class withRef extends React.Component {
    static displayName = `withRef(${WrappedComponent.displayName ||
      WrappedComponent.name ||
      "Component"})`;
    render() {
      // 这里重新定义一个props的原因是:
      // 你直接去修改this.props.ref在react开发模式下会报错,不允许你去修改
      const props = {
        ...this.props
      };
      // 在这里把getInstance赋值给ref,
      // 传给`WrappedComponent`,这样就getInstance能获取到`WrappedComponent`实例
      // 感谢评论区的[yangshenghaha]同学的完善
      props.ref = el => {
        this.props.getInstance && this.props.getInstance(el);
        this.props.ref && this.props.ref(el);
      };
      return <WrappedComponent {...props} />;
    }
  };
};

如果你的语法提示错误,不支持装饰器,你可以通过下面的设置:安装包依赖,npm install @babel/plugin-proposal-decorators。然后需要在package.json中设置插件的配置:

 "babel": {
    "plugins": [
      [
        "@babel/plugin-proposal-decorators",
        {
          "legacy": true
        }
      ]
    ],
    "presets": [
      "react-app"
    ]
  },

小结

通过本节,我们熟悉了react基本的父子组件、兄弟组件、跨级组件之间如何进行通讯,我们需要知道的额外一点是,通讯指的不仅仅是数据的通讯,也有事件的通讯,比如主动的根据时机去唤起父组件或者子组件的事件,在组件中我们可以很方便的通过属性调用父组件的方法或者传递的数据,我们也需要知道如何将子组件的事件暴露给父组件使用。

其实重点在下一篇,组件抽象,当然就是说的高级组件,只不过这次从《深入react技术栈》中得到了更多更全面的启示。点击跳转:react组件抽象