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

React 之 context 跨层级传值原理与重渲染范围

首页2018-07-23 09:50:12Front-End
JavaScriptReactcontextReact 原理

页面顶部有个主题切换按钮,切换之后整个页面几十个组件的配色都要跟着变。按 React 的单向数据流,主题值要从最外层一路作为 props 往下传,中间那些压根不关心主题的布局组件、卡片组件全都得加一个 theme 参数负责往下递。这种传法有个很形象的叫法,props drilling,钻井。

context 就是给这种场景准备的旁路通道。这篇把旧版 context 的写法完整讲一遍,讲清楚它为什么会被废弃,再把新 Context API 的订阅机制和重渲染范围说明白。重渲染范围这块尤其值得细看,很多人拿 context 当状态管理用,最后卡在这里。

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

  • context 解决的到底是哪一类问题,它和状态提升的分工
  • 旧版 getChildContext 加 contextTypes 的完整写法
  • react-redux 的 Provider 和 connect 是怎么用 context 实现的
  • 旧 context 被 shouldComponentUpdate 截断的致命缺陷
  • 新 Context API 的订阅模型,为什么它不会被截断
  • Provider 的 value 变化会引起多大范围的重渲染,怎么控制
  • 这套 API 到 React 19 的现状

# 一、简介

React 组件之间的通信是基于 props 的单向数据流,即父组件通过 props 向子组件传值,亦或是子组件执行传入的函数来更新父组件的 state,这都满足了我们大部分的使用场景。

一般地,对于兄弟组件之间的通信,是通过它们共同的祖先组件进行的,即 Lifting State Up,官方文档也有介绍。组件一通过事件将状态变更通知它们共同的祖先组件,祖先组件再将状态同步到组件二。

但是,如果组件之间嵌套得比较深,即使提升状态到共同父组件,再同步状态到相应的组件,还是需要一层一层地传递下去,可能会比较繁琐。

在对应的场景中,context 就可以缩短父组件到子组件的属性传递路径。

这里要先划一条线。context 缩短的是传递路径,它没有取消掉「状态放在上层」这件事。数据依然存在某个祖先组件的 state 里,依然是自上而下流动,变的只是中间那些组件不用再当搬运工。所以状态提升和 context 不是二选一的关系,是先提升,再决定要不要用 context 送下去。

什么时候值得用?我的判断标准很简单:这个值是不是「整棵子树都可能要,但中间大部分组件不关心」。主题、语言包、当前登录用户、路由信息,这几类都符合。反过来,一个只往下传两层的值,老老实实写 props 更清楚。

组件通信还有别的几种方式,各自的适用边界我在 React 组件通信方式全解 里做过横向对比。

# 二、旧版 context 的写法

下面这套写法是 React 16.3 之前的唯一选择,现在看已经过时了,但老项目里到处都是,而且理解它是理解新 API 为什么这么设计的前提。

先看一个完整例子,需求是让两个互不相邻的子组件通过共同祖先通信。

import Parent from './Parent'
import ChildOne from '../components/ChildOne'
import ChildTwo from '../components/ChildTwo'

export default class Container extends React.Component {
    constructor(props) {
        super(props);
        this.state = { value: '' }
    }

    changeValue = value => {
        this.setState({ value })
    }

    getChildContext() {
        return {
            value: this.state.value,
            changeValue: this.changeValue
        }
    }

    render() {
        return (
            <div>
                <Parent>
                    <ChildOne />
                </Parent>
                <Parent>
                    <ChildTwo />
                </Parent>
            </div>
        )
    }
}

Container.childContextTypes = {
    value: PropTypes.string,
    changeValue: PropTypes.func
}
@前端进阶之旅: 代码已经复制到剪贴板

Container 这个组件做了三件事:把值存在自己的 state 里、声明 childContextTypes 告诉 React「我要往下发这两个字段」、实现 getChildContext 返回具体的值。注意 getChildContext 会在每次渲染时被调用,返回的是当前这一刻的 state 快照。

中间这一层什么都不用做:

import React from "react"

const Parent = (props) => (
    <div {...props} />
)

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

Parent 完全不知道 context 的存在,它只是把 children 渲染出来。这正是 context 的价值所在,中间层是透明的。

下面是发送方 ChildOne:

export default class ChildOne extends React.Component {

    handleChange = (e) => {
        const { changeValue } = this.context
        changeValue(e.target.value)
    }

    render() {
        return (
            <div>
                子组件一
                <input onChange={this.handleChange} />
            </div>
        )
    }
}

ChildOne.contextTypes = {
    changeValue: PropTypes.func
}
@前端进阶之旅: 代码已经复制到剪贴板

接收方 ChildTwo:

export default class ChildTwo extends React.Component {
    render() {
        return (
            <div>
                子组件二
                <p>{this.context.value}</p>
            </div>
        )
    }
}

ChildTwo.contextTypes = {
    value: PropTypes.string
}
@前端进阶之旅: 代码已经复制到剪贴板

在 Container.childContextTypes 中进行接口的声明,通过 getChildContext 返回更新后的 state,在 Child.contextTypes 中声明要获取的接口,这样在子组件内部就能通过 this.context 获取到。通过 Context 这样一个中间对象,ChildOne 和 ChildTwo 就可以相互通信了。

有个很容易被忽略的细节:子组件的 contextTypes 不是可选的装饰,是必需的。你不声明 contextTypes,this.context 就是个空对象,什么都拿不到。这个设计当时是为了让依赖关系显式化,代价是每个消费方都得写一遍声明。上面几段代码用到了 PropTypes 却没有写 import PropTypes from 'prop-types',实际项目里记得补上,React 15.5 之后 PropTypes 已经从 React 主包里拆出去了。

# 三、多层嵌套下的传递

再看一个更贴近实际的例子,需求是在深层的导航栏里读到最外层 Page 的变量。

这类需求有两条路,用 context 传,或者用 props 一层层传。使用 context 的组件需要定义 propTypes,需要严格校验、声明类型、字段。

class Page extends React.Component {
    static childContextTypes = {
       user: PropTypes.string
    }
    constructor(props){
        super(props)
        this.state = {user:'poetries'}
    }
    getChildContext(){
        return this.state
    }
    render(){
        return (
          <div>
            <p>我是{this.state.user}</p>
            <Siderbar />
          </div>
        )
    }
}

class Siderbar extends React.Component {
    render(){
        return (
          <div>
            <p>侧边栏</p>
            <Navbar />
          </div>
        )
    }
}

class Navbar extends React.Component {
    static contextTypes = {
       user: PropTypes.string
    }
    render(){
        return (
          <div>
            <p>我是{this.context.user}的导航栏</p>
          </div>
        )
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

这段和原文相比我改了三处,都是原文的笔误,说明一下免得你照抄踩坑。

第一处,Siderbar 和 Navbar 原文写的是 childContextTypes,那是「我要往下发」的声明。它们俩是消费方不是提供方,应该写 contextTypes。写错的后果是 this.context.user 永远是 undefined,页面上什么都不显示,还不报错。

第二处,Siderbar 自己并不读 user,那它连 contextTypes 都不用写,直接透传就行。这恰好演示了中间层可以完全不感知 context。

第三处,原文的 Navbar 里又渲染了一个 <Siderbar />,而 Siderbar 里渲染 Navbar,这是个无限递归,跑起来直接爆栈。删掉了。

Page 里 getChildContext(){ return this.state } 这个写法能跑,但不推荐直接把整个 state 甩下去。context 是接口,接口应该只暴露需要暴露的字段,全量透传等于把内部实现全公开了,后面你往 state 里加个私有字段,整棵子树都能读到。

# 四、context 在 Provider 中的应用

理解了上面这套机制,react-redux 的实现原理就没什么神秘的了。Provider 组件就是使用 context,把 store 放到 context 里,所有的子元素可以直接取到 store。

import PropTypes from 'prop-types'

class Provider extends Component {
    static childContextTypes = {
        store: PropTypes.object
    }
    constructor(props, context){
        super(props, context)
        this.store = props.store
    }
    getChildContext(){
        //把传进来的store放进全局
        return {store: this.store}
    }
    render(){
        return this.props.children
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

这段代码就三十行不到,但它解释了为什么你在 React 应用最外层包一个 <Provider store={store}>,任意深度的组件都能连上 store。整个 react-redux 的「连接」能力,地基就是这个 getChildContext。

原文这里把 PropTypes 误写成了 Protypes,我改回来了。

fe
  • 一、简介
  • 二、旧版 context 的写法
  • 三、多层嵌套下的传递
  • 四、context 在 Provider 中的应用
  • 五、旧 context 为什么被废弃
  • 六、新 Context API 与重渲染范围
  • 总结
  • 参考

← React 组件通信方式全解与选型取舍手写一个迷你版 Redux,从 createStore 到中间件 →