• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

context 是 react 的主要特性,它能在任意层级的组件之间传递数据。

在业务代码中用 context 可能不多,大家更偏向于全局的状态管理库,比如 redux、mobx,但在 antd 等组件库里用的特别多。

那 antd 组件库是怎么用 context 的?context 又是怎么实现的呢?

今天我们就来探究一下。

首先,我们过一遍 context 的用法:

context 使用 createContext 的 api 创建:

import { createContext } from "react";

const countContext = createContext(111);

任意层级的组件可以从中取值:

function 组件使用 useContext 的 react hook:

import { useContext } from "react";

function Ccc() {
    const count = useContext(countContext);
    return <h2>context 值为:{count}</h2>;
}

class 组件使用 Consumer 的 api:

import { createContext, Component } from "react";

const countContext = createContext(111);

class Ccc extends Component {
    render() {
        return <h2>context 的 值为:{this.props.count}</h2>;
    }
}

function Bbb() {
    return (
        <div>
            <countContext.Consumer>
                {(count) => <Ccc count={count}></Ccc>}
            </countContext.Consumer>
        </div>
    );
}

修改 Context 中的值使用 Provider 的 api:

import { createContext } from "react";

const countContext = createContext(111);

function Bbb() {
    return (
        <div>
            <countContext.Provider value={333}>
                <Ccc></Ccc>
            </countContext.Provider>
        </div>
    );
}

总结来说就是用 createContext 创建 context 对象,用 Provider 修改其中的值, function 组件使用 useContext 的 hook 来取值,class 组件使用 Consumer 来取值。

这样的 context 机制就能实现任意层级的传值,比如这样三层组件:

import { createContext, useContext } from "react";

const countContext = createContext(111);

function Aaa() {
    return (
        <div>
            <countContext.Provider value={222}>
                <Bbb></Bbb>
            </countContext.Provider>
        </div>
    );
}

function Bbb() {
    return (
        <div>
            <Ccc></Ccc>
        </div>
    );
}

function Ccc() {
    const count = useContext(countContext);
    return <h2>context 的值为:{count}</h2>;
}

export default Aaa;

在 Aaa 组件中修改后的 context 值就能被 Ccc 组件拿到:

用起来其实还是蛮简单的,就这么几个用法,多写几遍就会了。

但有的同学可能会疑惑:context 好像在项目里也没咋用过呀,这个一般用来干啥呀?

真的不常用么?

并不是,antd 里就有大量的 context 应用,只是你不知道而已。

不信看下这个:

import { Form, Input } from "antd";
import { useEffect } from "react";

const App = () => {
    const [form] = Form.useForm();

    useEffect(() => {
        form.setFieldsValue({
            a: {
                b: {
                    c: "ccc",
                },
            },
            d: {
                e: "eee",
            },
        });
    }, []);

    return (
        <Form form={form}>
            <Form.Item name={["d", "e"]}>
                <Input />
            </Form.Item>
        </Form>
    );
};
export default App;

这代码熟悉吧!是不是经常写?

这是 antd 的 Form 组件的用法:

通过 useForm 拿到 form 对象,设置到 Form 组件里,然后用 form.setFieldsValue 设置的字段值就能在 Form.Item 里取到。

Form.Item 只需要在 name 里填写字段所在的路径就行,也就是 ['d', 'e'] 这个。

有的同学可能会问了,为啥这里只设置了个 name,它下面的 Input 就有值了呢?

我们让 Form.Item 渲染一个自定义的组件试一下,比如这样:

这时你会发现传入了 id、value、onChange 3 个参数:

这就是为啥 Input 能有值,因为传入了 value 参数。

而且变化了也能同步到 fields,因为传入了 onChange 参数。

有的时候我们要对保存的值做一些修改,就可以这样写:

function MyInput(props) {
    const { value, onChange } = props;
    function onValueChange(event) {
        onChange(event.target.value.toUpperCase());
    }
    return <Input value={value} onChange={onValueChange}></Input>;
}

我们调用 form.getFieldsValue 方法看看 onChange 前后的变化:

这时候 value 也会变,最终会改变表单的值:

所以说,Form.Item 会给子组件传入 value、onChange 参数用来设置值和接受值的改变,同步到 form 的 fields。

那这跟 context 有什么关系呢?

当然有呀,Form.Item 是怎么拿到 form 对象的呢?我们不是只传给了 Form 组件么,怎么会到了 Form.Item 手里的?

联系下刚学的 context api,是不是就能想清楚了?

肯定是有一个传递 form 对象的 context,Form 组件往其中设置值,Item 组件从其中取值。

我们看下源码就知道了:

Form 组件里用 useForm 创建了 form 对象,参数为 props 传入的 form。

然后它把这个 form 对象通过 Provider 放到了 FieldContext 里:

这个 FieldContext 自然是通过 createContext 的 api 创建的:

fieldContext 里就有 getFieldsValue、setFieldsValue 等 form 对象的方法了。

然后就是 Form.Item 了。

其实 Form.Item 里也渲染了一系列组件,主要是处理布局,这个用 React DevTools 调试下就知道了:

FormItem 加上了 Row、Col 等组件来布局,还加上了 Label 的部分,最后再渲染传入的 children。

其中有个 WrappedField 的子组件,这里面就取出了 FieldContext,作为参数传给了子组件:

而 namePath 也就是 ['d', 'e'] 的部分已经有了。

从 filedContext 里用 getFiledsValue 取出全部的 store,然后再通过 namePath 取出想要的值传给子组件,这不就完成了 Form.Item 的功能了么?

这就是为什么 form 里设置了 fields,在 Form.Item 里就能取出值来的原因。

小结一下:antd 的 Form 通过 FieldContext 保存了 form 对象,在 FormItem 组件里取出 FieldContext,并根据 namePath 取出对应的值,传递给子组件。这就完成了 form 的 field 值的设置。

除了 FieldContext 外,还有很多别的 Context,比如 size、disabled 等都是通过 context 存储和传递的:

在 antd 组件库里,context 有大量的应用。

那么 context 是怎么实现的呢?

这个要从源码找答案了。

首先是 createContext 方法,这个方法返回的是一个对象,记住这 3 个属性就可以了:

_currentValue: 保存 context 的值的地方,不建议直接改

Provider: ContextProvider 类型的 jsx

Consumer: ContextConsumer 类型的 jsx

也就是说这些 Provider、Consumer 都是单独的 jsx 类型:

react 渲染的时候会把 jsx 编译成 render function,然后执行之后就是 vdom:

image.png

vdom 是这样的结构,在 react 里也就是 ReactElement 对象:

Provider 就会变成这样的 vdom:

然后 vdom 会经历 reconcile 过程转为 fiber 结构,转完之后一次性 commit,也就是更改 dom。

这种 Provider 类型的 vdom 自然会转为对应的 fiber 节点,在 reconcile 的时候会做单独的处理:

可以看到 Provider 的处理就是修改了 context._currentValue 的值:

也就是说其实我们可以不用 Provider,自己修改 _currentValue 也可以。

只不过这种是不建议的。

总之,Provider 的原理就是修改了 context._currentValue。

然后再来看 useContext:

很容易想到,它就是读取了 context._currentValue 返回:

Consumer 的原理自然也差不多,也是读取了 context._currentValue,然后传入组件渲染:

那有的同学可能会问了,这不就是一个全局的对象么,然后 Provider 修改它的属性,Consumer 或者 useContext 读取它的属性。

这个自己封装不也行么?

还真不行。

因为 context 还有一个特别重要的特性:

比如这样的代码:

import { createContext, useContext } from "react";

const countContext = createContext(111);

function Aaa() {
    const count = useContext(countContext);

    return (
        <div>
            <h1>context 的 值为:{count}</h1>
            <Bbb></Bbb>
        </div>
    );
}

function Bbb() {
    return (
        <div>
            <countContext.Provider value={222}>
                <Ccc></Ccc>
            </countContext.Provider>
        </div>
    );
}

function Ccc() {
    const count = useContext(countContext);
    return <h2>context 的 值为:{count}</h2>;
}

export default Aaa;

也就是说在中间的组件里修改了 context 的值,那如果 context 是全局的话 Aaa、Ccc 组件的值都应该修改才对。

但实际上不是:

image.png

可以看到,只有 Ccc 拿到的 context 值被修改了,而 Aaa 拿到的 context 值没变。

这是为什么呢?

不是说都是取的 context._currentValue 这个属性么,咋还不一样了呢?

这是因为 react 对 context 还有一个处理:

在修改 context._currentValue 之前还有一个 push。

这个就是把当前的 context 值入栈:

之后处理完这个 fiber 节点会再 pop 出栈,然后恢复 context:

这就是为什么 context 只能影响子组件,影响不了父组件。

这就是 context 的原理。

小结一下:

createCotnext 就是创建了一个 _currentValue、Provider、Consumer 的对象。

_currentValue 就是保存值的地方

Provider 是一种 jsx 类型,之后会转为对应的 fiber 类型,然后它的处理就是修改 _currentValue,也就是修改 context 值

Consumer 和 useCotnext 就是读取 _currentValue,也就是读取 context 值

唯一要注意的是 Provider 处理每个节点之前会入栈 context,处理完会出栈,这样就能保证 context 只影响子组件。

总结

context 是 react 的重要特性,它主要用来在任意层级组件之间传递数据。

使用方式就是用 createContext 创建 context 对象,然后用 Provider 修改值,用 useContext 和 Consumer 读取值。

context 在 antd 这种组件库里用的特别多,比如 Form 的 fields 的值的传递,form.setFeildsValue 之后 FormItem 能拿到最新值就是通过 context 取的。

context 的原理是 context 对象有 _currentValue 属性用来保存值,Provider 会修改 _currentValue,Consumer 和 useContext 会读取它。

只是 Provider 还会入栈出栈机制保证值的修改只影响子组件。

context 原理其实还挺简单的,也就是一个对象属性的修改和读取。

它在 antd 组件库里用的太多了,比如 form 的 fields 传递、config 传递等等。不知道 context 用在哪的话,不妨去看下 antd 源码里怎么用的吧。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
44.Historyapi和ReactRouter实现原理
Next
46.ReactContext的性能缺点和解决方案