复习一下组件间通信方式

157 阅读11分钟

在我们开发使用 React 的过程中,经常需要组件之间相互传递信息,故复习一下我们常见的几个组件间的通信场景:

一、父组件向子组件通信

这是我们平时最常见也是用得最多的一种通信方式,父组件向子组件传递 props, 子组件通过获取 props 中的内容得到父组件传递的信息。

二、子组件向父组件通信

2.1 使用回调函数属性

这个方法也是react最常见的一种方式,父组件向子组件传递一个回调函数属性, 子组件通过调用父组件传递的回调函数, 从而将数据传给父组件,实现子组件向父组件通信,它的使用其实也是通过props进行传递,所以优缺点与上面描述的props一致,且如果层级较深,需要逐层传递回调函数,可能导致代码冗余和维护困难:

import React, { useState } from "react";
import { Input } from "antd";

const Childref = (props, ref) => {
  const [value, setValue] = useState("");
  return (
    <div>
      <Input
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
					props.updateValue(e.target.value);
        }}
        placeholder="我是子组件的输入框"
      />
    </div>
  );
};

class App extends React.Component{
	updateValue = (v) => {
		console.log(v, '我拿到了子组件输入框的值');
	}
	render(){
		return(
			<Childref updateValue={this.updateValue} />
		);
	}
}

2.2 使用ref

2.2.1 父组件、子组件均是class组件

在父组件中,用 React.createRef() 创建 ref 对象,将其赋值给组件的实例属性,将 ref 绑定到组件,在父组件可以用ref.current访问子组件上所有方法和属性:

class Child extends React.Component<any, IProps> {
  render() {
    return (
      <div>
        <Button
          onClick={() => {
            this.props?.callback("我收到了,你可以不要再传了");
          }}
        >
          点我
        </Button>
        父组件输入框的文案:{this.props.text}
      </div>
    );
  }
}
class App extends React.Component {
  ref = React.createRef(null);
  render() {
    return (
      <div>
        <Button
            onClick={() => {
              console.log(ref.current);
            }}
          >
            点我
          </Button>
        <Child ref={this.ref} />
      </div>
    );
  }
}
2.2.2 父组件是class组件,子组件是function组件

在父组件中,用 React.createRef() 创建 ref 对象,将其赋值给组件的实例属性,将ref绑定到组件上,但是我们可以看见,如果单单只是这样使用,会发现ref.current获取到的是null,所以这个时候我们要对function子组件进行forwardRef进行包裹,才能确保父组件能够访问到子组件的方法和属性:

import React, { useState, forwardRef } from "react";
import { Input } from "antd";

const Childref = forwardRef((props, ref) => {
	const [value, setValue] = useState("");
	return (
		<div ref={ref}>
		<Input
	value={value}
	onChange={(e) => {
		setValue(e.target.value);
	}}
	placeholder="我是子组件的输入框"
		/>
		</div>
	);
});
class App extends React.Component{
	ref = React.createRef(null);
	render() {
		return (
			<div>
				<Button
					onClick={() => {
						console.log(ref.current);
						// 提问:这时候打印出来的ref.current就是子组件的dom元素,如果想要获取子组件内部的属性和方法呢??
					}}
				>
				点我
				</Button>
				<Childref ref={ref} />
			</div>
		);
	}
}

回答

这时候就要用上useImperativeHandle了。【react版本16.8以后】

useImperativeHandle 接受三个参数:

第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
第三个参数 deps :为可选参数,为函数的依赖变量。凡是函数中使用到的数据变量都需要放入deps中,如果处理函数没有任何依赖变量,可以忽略第3个参数。依赖项改变的情况第一个参数会重新调用,如果依赖项不传则每次render都会调用。如果依赖项传一个空数组,则第一个方法只会在初始化的时候调用一次,里面的值不是最新的。

const Childref = forwardRef((props, ref) => {
  const [value, setValue] = useState("");
  useImperativeHandle(ref, () => ({
    value,
    setValue
  }));
  return (
    <div>
      <Input
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
        placeholder="我是子组件的输入框"
      />
    </div>
  );
});
class App extends React.Component{
	ref = React.createRef(null);
	render() {
		return (
			<div>
				<Button
					onClick={() => {
						console.log(ref.current);
						// 提问:如果想要获取子组件上的全部属性和方法呢?需要枚举式的在useImperativeHandle上挂载吗?
				>
				点我
				</Button>
				<Childref ref={ref} />
			</div>
		);
	}
}
2.2.3 父组件是function组件,子组件是class组件

在父组件中,用 useRef() 创建 ref 对象,将其赋值给组件的实例属性,将 ref 绑定到组件,在父组件可以用ref.current访问子组件上所有方法和属性:

class Child extends React.Component<any, IProps> {
  render() {
    return (
      <div>
        <Button
          onClick={() => {
            this.props?.callback("我收到了,你可以不要再传了");
          }}
        >
          点我
        </Button>
        父组件输入框的文案:{this.props.text}
      </div>
    );
  }
}
function App(){
  const ref = useRef(null);
    return (
      <div>
        <Button
            onClick={() => {
              console.log(ref.current);
            }}
          >
            点我
          </Button>
        <Child ref={ref} />
      </div>
    );
}
2.2.4 父组件、子组件均是function组件
import React, { useState, forwardRef } from "react";
import { Input } from "antd";

const Childref = forwardRef((props, ref) => {
	const [value, setValue] = useState("");
	return (
		<div ref={ref}>
		<Input
	value={value}
	onChange={(e) => {
		setValue(e.target.value);
	}}
	placeholder="我是子组件的输入框"
		/>
		</div>
	);
});
function App() {
	const ref = useRef(null);
	return (
		<div>
		<Button
			onClick={() => {
				console.log(ref.current);
				// 提问:这时候打印出来的ref.current就是子组件的dom元素,如果想要获取子组件内部的属性和方法呢??
			}}
		>
		点我
		</Button>
		<Childref ref={ref} />
		</div>
	);
}

总结

以上四种方式看出来两点,一个是父组件是class还是function组件只影响了在外面创建实例的方式,但是对于我们获取到的ref具体对象产生影响的是子组件的类型,如果是类组件,是无需做其他处理的 ,可以直接绑,但如果是函数式组件,本身是没有实例的,所以需要用forwardRef + useImperativeHandle进行辅助。

总结起来,useImperativeHandle的原理是通过将子组件内部定义的方法和属性绑定到ref对象上,从而使父组件可以通过ref.current访问和调用子组件的方法和属性。这样可以实现子组件向父组件暴露指定接口的目的,从而实现组件之间的通信和数据传递。

三、 跨级组件间通信

跨级组件是指父组件的子组件的子组件,或者更深层的嵌套关系,跨级组件之间想要通信,有两种常见方式:

3.1 中间组件层层传递

使用props将信息逐层传递

3.2 使用 Context 对象

如果要 Context 发挥作用,需要用到两种组件,一个是 Context 生产者(Provider),通常是一个父节点,另外是一个 Context 的消费者(Consumer),通常是一个或者多个子节点。所以 Context 的使用基于生产者消费者模式。使用步骤如下:

1、React.createContext创建了一个Context对象,假如某个组件订阅了这个对象,当react去渲染这个组件时,会从离这个组件最近的一个Provider组件中读取当前的context值

2、Context.Provider: 每一个Context对象都有一个Provider属性,这个属性是一个react组件。

在Provider组件以内的所有组件都可以通过它订阅context值的变动。具体来说,Provider组件有一个叫value的prop传递给所有内部组件,每当value的值发生变化时,Provider内部的组件都会根据新value值重新渲染

3、那内部的组件该怎么使用这个context对象里的东西呢?
a、对于react16.x之前的版本,内部组件为class声明的有状态组件:我们可以把Context对象赋值给这个类的属性contextType,如下面所示的GrandChildContext组件

b、对于react16.x之后的版本,我们可以使用Context.Consumer包裹子组件,这也是Context对象直接提供给我们的组件,这个组件接受一个函数作为自己的child,这个函数的入参就是context的value,并返回一个react组件。可以将上面的GrandChildContext改写为b:

c、对于react16.8之后的hooks的写法:我们可以使用useContext(context)去订阅provider的value值。可以将上面的GrandChildContext改写为c:

const ThemeContext = React.createContext("#fff");

export default class App extends React.Component {
  state = {
    value: "#000"
  };

  render() {
    return (
      <ThemeContext.Provider value={this.state.value}>
        <Button
          onClick={() => {
            this.setState({
              value: this.state.value === "green" ? "#000" : "green"
            });
          }}
        >
          变色
        </Button>
        <ChildContext />
      </ThemeContext.Provider>
    );
  }
}

function ChildContext() {
  return (
    <div>
      <GrandChildContext />
    </div>
  );
}
// a种情况
class GrandChildContext extends React.Component {
  static contextType = ThemeContext; // 16.3以及以上版本的区别
  render() {
    return <Button style={{ backgroundColor: this.context }} />;
  }
}
// b种情况
// function GrandChildContext() {
//   return (
//     <ThemeContext.Consumer>
//       {(value) => <Button style={{ backgroundColor: value }} />}
//     </ThemeContext.Consumer>
//   );
// }
// c种情况
// function GrandChildContext() {
// 		const theme = useContext(ThemeContext);
//   return (
//    <Button style={{ backgroundColor: theme }} />
//   );
// }

tips: context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性(很多组件都有可能依赖它们)时,才考虑用context。如果只是单纯为了解决层级很深的props传递,需要考虑其他方式。

局限性:

  • 在组件树中,如果中间某一个组件 ShouldComponentUpdate returning false 了,会阻碍 context 的正常传值,导致子组件无法获取更新。
  • 组件本身 extends React.PureComponent 也会阻碍 context 的更新。

四、非嵌套组件间通信

非嵌套组件,就是通信组件间没有任何包含关系,包括兄弟组件以及不在同一个父级中的非兄弟组件;对于非嵌套组件,可以利用这几种方式通信:

4.1 利用二者共同父组件进行通信

这种情况相当于将父组件当作数据管理中间商了,两个兄弟组件无法直接沟通,那就让父组件暂存并转达一下消息,这种情况在我们项目中也经常用到,不举例了。

4.2 使用发布订阅的方式

发布订阅PubSubJS GitHub地址**
**如果采用组件间共同的父级来进行中转,会增加子组件和父组件之间的耦合度,如果组件层次较深的话,找到二者公共的父组件不是一件容易的事,使用发布订阅的方式来实现非嵌套组件间的通信,但他的缺点是事件订阅和发布的管理可能较为复杂,不易维护。

import React, { Component } from "react";
import PubSub from "pubsub-js";

export default class List extends Component {
  state = {
    //初始化状态
    users: [], //users初始值为数组
    isFirst: true, //是否为第一次打开页面
    isLoading: false, //标识是否处于加载中
    err: "" //存储请求相关的错误信息
  };
  token: any = null;

  componentDidMount() {
    this.token = PubSub.subscribe("refresh", (_, stateObj) => {
      this.setState(stateObj);
    });
  }

  componentWillUnmount() {
    PubSub.unsubscribe(this.token);
  }

  render() {
    const { users, isFirst, isLoading, err } = this.state;
    return (
      <div className="row">
        {isFirst ? (
          <h2>欢迎使用,输入关键字,随后点击搜索</h2>
        ) : isLoading ? (
          <h2>Loading......</h2>
        ) : err ? (
          <h2 style={{ color: "red" }}>{err}</h2>
        ) : (
          users.map((userObj) => {
            return (
              <div key={userObj.id} className="card">
                <p className="card-text">{userObj.name}</p>
              </div>
            );
          })
        )}
      </div>
    );
  }
}
import React, { Component } from "react";
import PubSub from "pubsub-js";
import axios from "axios";

export default class Search extends Component {
  keyWordElement: any = {
    value: ""
  };
  search = () => {
    //获取用户的输入(连续解构赋值+重命名)
    const {
      keyWordElement: { value: keyWord }
    } = this;
    //发送请求前通知List更新状态
    PubSub.publish("refresh", { isFirst: false, isLoading: true });
    //发送网络请求
    setTimeout(() => {
      PubSub.publish("refresh", {
        isLoading: false,
        users: [
          {
            id: 1,
            name: "这是一条假数据"
          }
        ]
      });
    }, 2000);
  };

  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">搜索github用户</h3>
        <div>
          <input
            ref={(c) => (this.keyWordElement = c)}
            type="text"
            placeholder="输入关键词点击搜索"
          />
          &nbsp;
          <button onClick={this.search}>搜索</button>
        </div>
      </section>
    );
  }
}

4.3 使用redux通信

这种方式是我们目前比较大面积使用的,因为很多数据不会频繁更新,但需要在很多并不一定有关系的组件中都需要用到,所以我们将数据都存在redux中进行统一管理,这样所有的组件都可以通过connect绑定我们的状态池获取自己当前所需要的数据进行操作。它的缺点我们也都有感触,就是代码复杂,需要引入额外的依赖和配置,对于简单场景会增加开发成本【此处不举例啦~】

4.4 Global Variables

哈哈哈,这种肯定轻易不要使用啦,我们项目中只在配置文件中用到了global variables,但是并不是用于组件之间通信,只适用于方便管理和运维,严格意义上来说可以算是开发与运维、测试之间的通信吧~

五、对比

方式使用场景优点缺点
props单向数据流、父子组件之间的简单通信简单易用,适用于简单组件间的通信。只适用于父子组件之间的通信,组件层级比较较深时传递起来就比较麻烦。
ref需要访问子组件的实例或DOM元素、触发子组件的方法的时候灵活性高,可以直接访问子组件的实例或者dom元素。破坏了React的数据流一致性,不推荐过度使用。
callback子组件向父组件传递数据或组件之间的双向通信简单明了,适用于父子组件之间的通信或组件之间的双向通信如果层级较深,需要逐层传递回调函数,可能导致代码冗余和维护困难
context可用于像Theme、Language等设置类全局属性,或跨组件层级的数据共享,避免逐层传递数据可以直接访问子组件的实例或DOM元素,灵活性高上下文的更新会引发整个消费者子树的重新渲染,性能开销较大。
发布订阅非父子组件之间的通信、跨组件层级的通信灵活、适用于非父子组件之间的通信。事件订阅和发布的管理可能较为复杂,不易维护。
redux多个组件共享状态、组件之间的复杂通信全局状态管理,方便组件之间的通信和状态共享代码复杂,需要引入额外的依赖和配置,对于简单场景会增加开发成本。
全局变量Global Variables适用于极少数地方修改、而多处使用且不易重复的变量名,但不推荐大面积使用全局可见,任何一个函数或线程都可以读写全局变量-同步操作简单;内存地址固定,读写效率比较高。过多的全局变量会占用较多的内存单元;破坏了函数的封装性能;使函数的代码可读性降低.

总结

本文记录了不同嵌套关系的组件间常用的几种通信方式:

  1. 父子嵌套关系:利用 props 对象实现父组件向子组件通信;
  2. 父子嵌套关系:利用 callback(回调函数) 或ref实现子组件向父组件通信;
  3. 多层(父子)嵌套关系(跨级通信):利用 Context 对象, 以生产者和消费者的方式实现通信;
  4. 非嵌套关系:利用 PubSubJS (发布订阅) 的方式实现通信;
  5. 示例:以上提到的一些通信方式****