React ContextAPI 的各种用法

861 阅读4分钟

React Context API

主要理解新版 Context API 的如下内容:

  • 基本概念
  • Context 要解决的问题
  • Context 使用方法
  • Context 源码解析
  • 对比旧版 Context API

基本概念

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

Context 要解决的问题

问题: 当组件的层次较为深的时候,Props 的传递数据变得重复而繁琐,代码可维护性也变差,Context 相当于一个数据中心,当组件需要数据的时候,把这些公共的数据放在数据中心统一的读取。

Context API

React.createContext 主要作用是 跨越组件 传递数据。

Context 使用方式

使用 class 组件

import React from 'react';

const MyContext = React.createContext()

export default class Text extends React.Component {

  state = {
    Shu: 1
  }

  setShu() {
    this.setState({ Shu: ++this.state.Shu })
  }

  render() {
    return (
      (
        <div>
          <MyContext.Provider value={this.state.Shu}>
            <Parent></Parent>
          </MyContext.Provider>
          <button onClick={this.setShu.bind(this)}>加加</button>
        </div>
      )
    )
  }
}

class Parent extends React.Component {
  render() {
    return (
      <div>
        <Child></Child>
      </div>
    )
  }
}

class Child extends React.Component {
  static contextType = MyContext;
  render() {
    return <div>{this.context}</div>
  }
}

// 也可将静态属性放在外部,然后直接在内部使用 this.context 进行访问
// Child.contextType = MyContext

class 是提供一个内置的静态属性 contextType, 有了这个属性之后,我们就能通过 this.context 来访问 context 携带到的数据了。第二种方式就是通过 Consumer 来获取数据了。

函数式组件中使用

  1. 函数式组件不受用 Context.Provider/Context.Consumer
import React from 'react';

const MyContext = React.createContext()

 let initialState = 1
export default function Text() {
  
  let [Shu, setShu] = React.useState(initialState)

  const handleSetShuFu = () => {
    setShu(++Shu)
  }
  return (
    <div>
      <MyContext.Provider value={Shu}>
        <Parent></Parent>
      </MyContext.Provider>
      <button onClick={handleSetShuFu}>加加</button>
    </div>
  )
}

function Parent() {
  return (
    <div>
      <Children></Children>
    </div>
  )
}

function Children() {
  return (
    <div>
      <MyContext.Consumer>
        { value => <div>{value}</div>}
      </MyContext.Consumer>
    </div>
  )
}

提供者和消费者是成对的。 提供者 Provider 通过 value 向下传递数据,在我们要使用的 Provider 提供的值的时候,我们就需要使用对应的 Consumer, Consumer 对应的值通过函数可以访问到。

  1. 函数式组件中使用 context api 配合钩子函数 React.useContext 使用。
import React from 'react';

const MyContext = React.createContext()

export default function TextContext() {
  
  let [ShuFu, setShuFu] = React.useState(1) // 传递一个几个基本类型的数据

  const handleSetShuFu = () => {
    setShuFu(++ShuFu)
  }
  return (
    <div>
      <MyContext.Provider value={ShuFu}>
        <Parent></Parent>
      </MyContext.Provider>
      <button onClick={handleSetShuFu}>加加</button>
    </div>
  )
}

function Parent() {
  return (
    <div>
      <Children></Children>
    </div>
  )
}

function Children() {
  const ctx = React.useContext(MyContext)
  return (
    <div>{ctx}</div>
  )
}

使用钩子函数的流程:创建一个 context 上下文,拿到上下文的组件 Provider。 然后在 Provider value 属性中添加数据。 然后在子函数组件中使用 useContext 来获取 context 数据。

总结

使用 React hooks 可以更加简单的创建 context 和使用 context, 但是又有了学习成本,但是 hook 绝对是值的学习的。

更新 context 中的数据

上面例子中,更新 context 的数据还是在顶层组件,也可以在消费处 MyConsumer,进行 context 数据的更新。思路就是使用在 Provider 中多提供一个函数,在消费处 MyConsumer 处使用事件来调用这个函数即可。

多个 Context 的消费能力

当我们要消费多个 context 的时候,提供者和和消费者都是使用嵌套,在消费者中,我们可以使用钩子函数磨平嵌套的消费者使用

import React from 'react'

let MyContext1 = React.createContext()
let MyContext2 = React.createContext()

export default function Muitle () {

  let [Ad, setAd] = React.useState(1)
  let [Bd, setBd] = React.useState(5)
  
  return (
    <div>
      <MyContext1.Provider value={Ad}>
        <MyContext2.Provider value={Bd}>
          <Parent />
        </MyContext2.Provider>
      </MyContext1.Provider>

      <button onClick={() => setAd(++Ad)}>++Ad</button>
      <button onClick={() => setBd(++Bd)}>++Bd</button>
      <button onClick={() => setAd(--Ad)}>--Ad</button>
      <button onClick={() => setBd(--Bd)}>--Bd</button>
    </div>
  )
}

function Parent() {
  return (
    <div>
      <Child></Child>
    </div>
  )
}

function Child () {
  const c1 = React.useContext(MyContext1)
  const c2 = React.useContext(MyContext2)

  return (
    <div>
      { c1 } + { c2 } = { c1 + c2 }
    </div>
  )
}

问题

当我们传递的context 是一个引用类型的值的时候,我们的数据更新就出现不是预期的效果

源码解析

import {createContext} from './ReactContext';
import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';

import type {ReactContext} from 'shared/ReactTypes';

import warningWithoutStack from 'shared/warningWithoutStack';
import warning from 'shared/warning';

export function createContext<T>(
  defaultValue: T,
  calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null;
  } else {
    // dev code ...
  }

  const context: ReactContext<T> = {
    $typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
  };

  context.Provider = {
    $typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;

  if (__DEV__) {
    // dev code ...
  } else {
    context.Consumer = context;
  }

  return context;
}

简单来看 context 的源码是很简单的就是创建了一个 context 的对象,添加了 Provider 和 Comsumer 属性。

useContext 源码

export function useContext<T>(
  Context: ReactContext<T>,
  unstable_observedBits: number | boolean | void,
) {
  const dispatcher = resolveDispatcher();
  // dev code...
  return dispatcher.useContext(Context, unstable_observedBits);
}

useContext 源码看起来还是很简单,即派发 useContext, 这个背后设计到了 React 的整个协调过程,协调是一个非常复杂的问题,可以单独的写很多专题的文章,这里就不在纠结 useContext 背后做了什么事情,具体源码后续在协调过程会有介绍,届时会同步更新。

参考

  1. Context API 中文

本文使用 mdnice 排版