前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版

React 组件通信方式全解与选型取舍

首页2018-07-29 23:20:24Front-End
组件通信Reactcontext前端进阶

一个中后台页面写到第三个迭代,组件树已经有五六层深。这时候产品说,筛选栏改了条件之后,右上角的导出按钮要跟着变成禁用。筛选栏和导出按钮离得很远,中间隔了四层布局组件。这个 prop 该怎么传?

React 的数据流规则本身很简单,就一句「单向、自上而下」,但真到了工程里,组件之间的位置关系千变万化,能用的手段也不止 props 一种。这篇把四类通信场景的写法都过一遍,重点不在于代码怎么写,而在于什么情况下该选哪一种,以及选错了会付出什么代价。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 父传子、子传父、跨级、无嵌套关系四类场景各自的标准解法
  • 回调函数传参时 bind 和箭头函数的取舍
  • 跨级通信为什么官方长期不推荐 context
  • 事件总线的写法,以及它为什么容易把项目搞乱
  • 四种方式的横向对比和选型判断
  • Hooks 时代这几种写法各自变成了什么样

React 组件通信的四类场景示意图

组件之间进行通信的几种情况:

  • 父组件向子组件通信
  • 子组件向父组件通信
  • 跨级组件通信
  • 没有嵌套关系组件之间的通信

# 一、父组件向子组件通信

React 数据流动是单向的,父组件向子组件通信也是最常见的。父组件通过 props 向子组件传递需要的信息。

// Child.jsx
import React from 'react';
import PropTypes from 'prop-types';

export default function Child({ name }) {
    return <h1>Hello, {name}</h1>;
}

Child.propTypes = {
    name: PropTypes.string.isRequired,
}


// Parent.jsx
import React, { Component } from 'react';

import Child from './Child';

class Parent extends Component {
    render() {
        return (
            <div>
                <Child name="Sara" />
            </div>
        );
    }
}

export default Parent;
@前端进阶之旅: 代码已经复制到剪贴板

这段代码没什么可解释的,但有两个点值得多说。

一是 propTypes 加 isRequired 这个习惯。它在开发环境下会做运行时校验,父组件忘了传 name 会直接在控制台报警告。2018 年这是标配,今天更常见的做法是用 TypeScript 在编译期就把类型卡死,效果更好也更早。顺带一提,React 19 已经移除了对函数组件 propTypes 的支持,老项目升级时这类声明会静默失效,得留意。

二是「父传子」这条路走得再远也是有代价的。开头那个筛选栏到导出按钮的例子,如果硬用 props 传,中间四层布局组件每一层都要加一个自己根本不用的参数往下递。这种传法通常叫 props drilling,层数一多,改一个字段名要动五个文件。

所以 props 适合的是两三层以内的直接传递。再深就该考虑第三节的方案了。

# 二、子组件向父组件通信

数据是单向往下流的,那子组件怎么往上说话?两条路:

  • 利用回调函数
  • 利用自定义事件机制

回调函数是主流。原理很朴素,父组件把一个自己的方法当 props 传下去,子组件在合适的时机调它,函数体是在父组件的作用域里跑的,自然就能改父组件的 state。

下面这个例子实现的功能是,在子组件中点击隐藏组件按钮可以将自身隐藏。

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class List3 extends Component {
    static propTypes = {
        hideConponent: PropTypes.func.isRequired,
    }
    render() {
        return (
            <div>
                哈哈,我是List3
                <button onClick={this.props.hideConponent}>隐藏List3组件</button>
            </div>
        );
    }
}

export default List3;
@前端进阶之旅: 代码已经复制到剪贴板

子组件这一侧非常干净,它甚至不知道点了之后会发生什么,只负责在按钮被点的时候把消息喊出去。这种「子组件不持有业务语义」的设计是对的,换个父组件复用它,行为就可以完全不同。

父组件这一侧:

//app.jsx

import React, { Component } from 'react';

import List3 from './components/List3';
export default class App extends Component {
    constructor(...args) {
        super(...args);
        this.state = {
            isShowList3: false,
        };
    }
    showConponent = () => {
        this.setState({
            isShowList3: true,
        });
    }
    hideConponent = () => {
        this.setState({
            isShowList3: false,
        });
    }
    render() {
        return (
            <div>
                <button onClick={this.showConponent}>显示Lists组件</button>
                {
                    this.state.isShowList3 ?
                        <List3 hideConponent={this.hideConponent} />
                    :
                    null
                }

            </div>
        );
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

真正的状态 isShowList3 在父组件手里,子组件只能通过调函数来请求改变。这就是「状态提升」的标准形态,也是 React 里兄弟组件通信的默认解法:两个兄弟要通信,把共享的那部分 state 提到它们最近的共同父组件上去。

这里有个坑要注意,showConponent = () => {} 这种类属性加箭头函数的写法不是随便写的,它保证了 this 绑定。如果你写成普通的类方法 showConponent() {},传下去之后 this 就丢了,调用时会报 Cannot read property 'setState' of undefined。老代码里常见的替代方案是在 constructor 里写 this.showConponent = this.showConponent.bind(this)。

需要传参的时候,写法是 onClick={() => this.handleClick(id)}。这样每次渲染都会生成一个新的函数引用,如果子组件做了 React.memo 或者 shouldComponentUpdate 的浅比较优化,这个新引用会让优化直接失效。数据量小无所谓,长列表里就得注意,通常的做法是把 id 通过 data- 属性挂在 DOM 上,或者把每一项抽成独立组件,让每一项自己持有自己的 id。

# 三、跨级组件通信

跨级指的是隔了不止一层,比如爷爷组件要和孙子组件说话。

# 3.1 层层组件传递 props

例如 A 组件和 B 组件之间要进行通信,先找到 A 和 B 公共的父组件 C,A 先向 C 组件通信,C 组件通过 props 和 B 组件通信,此时 C 组件起的就是中间件的作用。

这条路的好处是不引入任何新概念,数据流向一眼能看清,坏处第一节已经说了,层数一多就是灾难。

我自己的判断线大概在三层。三层以内老实传,超过三层就该换方案了。

# 3.2 使用 context

context 是一个全局变量,像是一个大容器,在任何地方都可以访问到。我们可以把要通信的信息放在 context 上,然后在其他组件中可以随意取到。

但是 React 官方不建议使用大量 context,尽管它可以减少逐层传递,但是当组件结构复杂的时候,我们并不知道 context 是从哪里传过来的;而且 context 是一个全局变量,全局变量正是导致应用走向混乱的罪魁祸首。

这段是 2018 年的判断,那时候官方的态度确实很保守。原因不只是「全局变量容易乱」这一条,更硬的技术原因是旧版 context 的传播会被中间组件的 shouldComponentUpdate 截断,导致值改了但下游收不到。React 16.3 换成新的 createContext 之后这个缺陷被修掉了,官方态度也从「不推荐」转成了「主题、语言、当前用户这类场景推荐用」。这一段的机制细节我在 React 之 context 跨层级传值原理 里展开讲了,包括新 API 的重渲染范围问题。

下面例子中的组件关系是,ListItem 是 List 的子组件,List 是 app 的子组件。

// ListItem.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class ListItem extends Component {
    // 子组件声明自己要使用context
    static contextTypes = {
        color: PropTypes.string,
    }
    static propTypes = {
        value: PropTypes.string,
    }
    render() {
        const { value } = this.props;
        return (
            <li style={{ background: this.context.color }}>
                <span>{value}</span>
            </li>
        );
    }
}

export default ListItem;
@前端进阶之旅: 代码已经复制到剪贴板

消费方要显式声明 contextTypes,不声明的话 this.context 就是空对象。这个强制声明当年是为了让隐式依赖变得可见。

提供方这一侧:

// List.jsx
import ListItem from './ListItem';

class List extends Component {
    // 父组件声明自己支持context
    static childContextTypes = {
        color: PropTypes.string,
    }
    static propTypes = {
        list: PropTypes.array,
    }
    // 提供一个函数,用来返回相应的context对象
    getChildContext() {
        return {
            color: 'red',
        };
    }
    render() {
        const { list } = this.props;
        return (
            <div>
                <ul>
                    {
                        list.map((entry, index) =>
                            <ListItem key={`list-${index}`} value={entry.text} />,
                       )
                    }
                </ul>
            </div>
        );
    }
}

export default List;
@前端进阶之旅: 代码已经复制到剪贴板

注意 List 把 color 发下去之后,ListItem 就能直接读到,中间不需要任何 props 中转。这就是 context 省掉的那部分工作。

最外层:

fe
  • 一、父组件向子组件通信
  • 二、子组件向父组件通信
  • 三、跨级组件通信
    • 3.1 层层组件传递 props
    • 3.2 使用 context
  • 四、没有嵌套关系的组件通信
  • 五、四种方式怎么选
  • 六、Hooks 时代这些写法变成了什么样
  • 总结
  • 参考

← 小程序入门总结篇React 之 context 跨层级传值原理与重渲染范围 →