React 的源码与原理解读(十六):Context 与 useContext

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

这个章节是 React 源码系列的最后一个章节了,主要来讲我们用于跨多层级通信的常用 hooks —— useContext,我们会从 context 的创建和消费讲起,再讲到怎么样使用 useContext 这个钩子来简化操作,最后分析 context 的运行原理和挂载过程。

Context 的定义

Context 是 React 官方提供的一种让父组件可以为它下面的整个组件树提供数据的数据传递方式,它产生的原因是:

  • props 是将数据通过 UI 树显式传递到使用它的组件的好方法。但是当你需要在组件树中深层传递参数以及需要在组件间复用相同的参数时,传递 props 就会变得很麻烦。最近的根节点父组件可能离需要数据的组件很远,而状态提升到太高的层级会导致 “逐层传递 props” 的情况。

  • Context 多层级之间则是一种不需要 props 将数据“直达”到所需的组件中的数据传递方式

Context 的使用

在 React 中,Context 的使用步骤如下:

  1. 创建 一个 context。
  2. 在指定数据的组件中 提供 这个 context。
  3. 在子组件中 消费 这个 context

下面是一个具体的例子:

首先我们使用 createContext 这个 API 来创建一个 Context ,它传入一个初始值,返回一个 context

const Context = React.createContext('default-value')

我们可以通过 Provider 包裹组件来提供这个 context,其中的 value 就是给子组件的这个 Provider 的初始值,下面的相当于 Context.Provider 包裹的所有子组件都可以通过 Context 来获取相同的数据,这些数据的初始值是 new-value。

注意:Provider 中提供的值才是 context 的默认值,createContext 初始化的值并不是默认值,只有当 Provider 未提供默认值时才会使用定义时的默认值。

const Context = React.createContext('default-value')
function Parent() {
    
    
 return ( 
  // 在内部的后代组件都能够通过相同的 Ract.createContext() 的实例访问到 context 数据
  <Context.Provider value="new-value">
     <Children>
  <Context.Provider>
 )
}

而我们的子组件可以通过的来消费我们的 context ,这里我们需要从之前定义 Context 的位置将其引入,只有使用了同一个 Context 才能获取相同的数据

import Context from "xxxxxx"
<Context.Consumer>
      {
    
     v => {
    
    
        // 内部通过函数访问祖先组件提供的 Context 的值
        return <div> {
    
    v} </div>
      }}
</Context.Consumer>

我们可以通过修改 Provider 提供的值来改变 Context 的内容,以下是一个例子:

  • 我们将一个 language 和改变这个 language 的方法传入作为一个 context 提供给子组件
  • 子组件通过 Consumer 接收这些内容,子组件点击按钮调用 Provider 提供的方法来改变 language
  • 注意的一点是:当 Context.Providervalue 值发生变化时,所有使用 Context 的组件会强制更新
class App extends Component {
    
    
  setLanguage = language => {
    
    
    this.setState({
    
     language });
  };

  state = {
    
    
    language: "en",
    setLanguage: this.setLanguage
  };
    
  render() {
    
    
    return (
      <LanguageContext.Provider value={
    
    this.state}>
        <h2>Current Language: {
    
    this.state.language}</h2>
        <p>Click button to change to jp</p>
        <div>
          <LanguageSwitcher />
        </div>
      </LanguageContext.Provider>
    );
  }
}

class LanguageSwitcher extends Component {
    
    
  render() {
    
    
    return (
      <LanguageContext.Consumer>
        {
    
    ({
     
      language, setLanguage }) => (
          <button onClick={
    
    () => setLanguage("jp")}>
            Switch Language (Current: {
    
    language})
          </button>
        )}
      </LanguageContext.Consumer>
    );
  }
}

useContext 的定义和使用

useContext 是一个 React Hook,可以让你读取和订阅组件中的 Context

const value = useContext(SomeContext)

它其实就是简化了我们消费 Context 的过程,我们不必再通过 Consumer 来获取需要的数据,而只要通过 useContext 就可以拿到 Context 内部的值:

import Context from "xxxxxx"
function Child() {
    
    
  const {
    
     ctx } = useContext(Context)
  return <div> {
    
    ctx} </div>
}

Context 的使用场景

  • Context 常常用在某个状态需要提供给多个子组件、以及子组件的子组件使用的情况下,比如登录状态管理黑夜模式
  • Context 还可以方便的用在爷孙组件之间传参,如果一个组件的孙子组件需要使用其爷爷组件的一个状态,正常情况下需要经过多层的 props 传递,但是使用 Context 之后就可以避免这样复杂的流程

Context 的源码

创建 Context

现在到了我们的重头戏,关于 context 的原理,我们先从 context 这个类的定义说起,它在我们的 packages/shared/ReactTypes.js 这个文件中:

export type ReactContext<T> = {
    
    
  $$typeof: Symbol | number,
  Consumer: ReactContext<T>,           // 消费 context 的组件
  Provider: ReactProviderType<T>,      // 提供 context 的组件
  // 保存 2 个 value 用于支持多个渲染器并发渲染
  _currentValue: T,
  _currentValue2: T,
  _threadCount: number, // 用来追踪 context 的并发渲染器数量
  // DEV only
  _currentRenderer?: Object | null,
  _currentRenderer2?: Object | null,
    
  displayName?: string,  // 别名
  _defaultValue: T,      
  _globalName: string,
  ...
};

createContext 就是新建了这样一个数据结构,包括了数据、Consumer 和 Provider 来提供用户使用,它的代码在 packages/react/src/ReactContext.js 这个文件中:

export function createContext<T>(defaultValue: T): ReactContext<T> {
    
    
    
  const context: ReactContext<T> = {
    
    
    $$typeof: REACT_CONTEXT_TYPE,   // 用 $$typeof 来标识这是一个 context
    _currentValue: defaultValue,    // 给予初始值
    _currentValue2: defaultValue,   // 给予初始值
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
    _defaultValue: (null: any),
    _globalName: (null: any),
  };
	
  // 添加 Provider ,并且 Provider 中的_context指向的是 context 对象
  context.Provider = {
    
    
    $$typeof: REACT_PROVIDER_TYPE,   // 用 $$typeof 来标识这是一个 Provider 的 symbol
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;
  let hasWarnedAboutDisplayNameOnConsumer = false;
	
  // 添加 Consumer
  context.Consumer = context;
  return context;
}

提供 Context

在新建了我们的 Context 后,接下来就是 Context 的提供,我们知道 Context 使用 Provider 来提供 Context 内容,而这个 <Context.Provider> 则是作为了一个 DOM 元素节点编写在我们的 jsx 代码中,那它的值是怎么样被提供给子元素的呢,我们来看:

上文中我们提到了,我们使用 $$typeof 来标识一个 Provider ,读过之前教程的读者应该对这个不陌生,我们在第一篇教程中提到了,我们的 ReactElement 中就是用这个字段来标识这是一个 react.element。同样,这里我们也用这个字段来标识 Provider 元素,这样我们在生成 Fiber 的时候就可以进行统一的处理。

代码在我们的 /packages/react-reconciler/src/ReactFiber.old.js 函数中,我们调用了 createFiberFromTypeAndProps 建立 Fiber,其中,我们通过传入的 type来判定,其中根据 $$typeof 的值给我们的 Fiber 的 tag 添加了不同的值,上文中,在创建 context 时,Provider 给予了 REACT_PROVIDER_TYPE 类型,而 Consumer 指向 context 本身,所以就是 REACT_CONTEXT_TYPE 类型字段,因而,当我们在 jsx 中解析到这两个类型时,就会判定为对应的字段:

export function createFiberFromTypeAndProps(
  type: any, // React$ElementType,element的类型
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
    
    
  let fiberTag = IndeterminateComponent;
  let resolvedType = type;
  if (typeof type === 'function') {
    
    
	// ....
  } else if (typeof type === 'string') {
    
    
	// ....
  } else {
    
    
    getTag: switch (type) {
    
    
	// ....
      default: {
    
    
        if (typeof type === 'object' && type !== null) {
    
    
          switch (type.$$typeof) {
    
    
            case REACT_PROVIDER_TYPE:
              fiberTag = ContextProvider;
              break getTag;
            case REACT_CONTEXT_TYPE:
              fiberTag = ContextConsumer;
              break getTag;
			//.....
          }
        }
      }
    }
  }

在判定了对应的类型后,我们继续看对 Fiber 的处理:我们在 beginWork 这个函数中,我们会对不同 tag 的进行处理,我们先来看ContextProvider 的处理:

  • 首先我们获取当前传入的 pendingProps ,也就是我们传入的 props ,再其中拿到 value 这个我们传入的值
  • 之后我们调用 pushProvider 这个函数 ,他修改了 context 的 _currentValue ,也就是更新了 context 的值
  • pushProvider 函数还做了一个压栈的操作,这个操作我们会在后续详细来说
  • 之后我们判定是不是可以复用(这个我们之前已经详细提过了)
  • 如果不不能复用,我们需要通过 propagateContextChange 方法标记我们的更新
function updateContextProvider(current, workInProgress, renderLanes) {
    
    
  const providerType = workInProgress.type
  const context = providerType._context
  const newProps = workInProgress.pendingProps
  const oldProps = workInProgress.memoizedProps
  const newValue = newProps.value
  pushProvider(workInProgress, context, newValue)
  // 是更新
  if (oldProps !== null) {
    
    
    const oldValue = oldProps.value
    // 可以复用
    if (is(oldValue, newValue)) {
    
    
      if (
        oldProps.children === newProps.children &&
        !hasLegacyContextChanged()
      ) {
    
    
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        )
      }
    } else {
    
    
      // 查找 consumer 消费组件,标记更新
      propagateContextChange(workInProgress, context, renderLanes)
    }
  }
  // 继续遍历
  const newChildren = newProps.children
  reconcileChildren(current, workInProgress, newChildren, renderLanes)
  return workInProgress.child
}

function pushProvider(providerFiber, context, nextValue) {
    
    
  // 压栈
  push(valueCursor, context._currentValue, providerFiber)
  // 修改 context 的值
  context._currentValue = nextValue
}

function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
    
    
  index++;
  valueStack[index] = cursor.current;
  cursor.current = value;
}

我们发现如果我们需要更新我们的 context ,它会调用 propagateContextChange 这个方法来标记更新,那么它的具体逻辑是什么呢?它主要调用了 propagateContextChange_eager 这个函数,我们来看一下这个函数:

深度优先遍历所有的子代 fiber ,然后找到里面具有 dependencies 的属性,,这个属性中挂载了一个元素依赖的所有 context,它的挂载会在下一节中提到,对比 dependencies 中的 context 和当前 Provider 的 context 是否是同一个;如果是同一个,它会创建一个更新,设定高 fiber 的更新优先级,类似于调用 this.forceUpdate 带来的更新:

function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {
    
    
  let fiber = workInProgress.child;
  if (fiber !== null) {
    
    
    fiber.return = workInProgress;
  }
  // 深度优先遍历整个 fiber 树
  while (fiber !== null) {
    
    
    let nextFiber;
    const list = fiber.dependencies;
    if (list !== null) {
    
    
      nextFiber = fiber.child;
      let dependency = list.firstContext;
      // 获取 dependencies
      while (dependency !== null) {
    
    
        // 如果是同一个 context
        if (dependency.context === context) {
    
    
          if (fiber.tag === ClassComponent) {
    
    
            const lane = pickArbitraryLane(renderLanes);
            const update = createUpdate(NoTimestamp, lane);
            // 高优先级,强制更新
            update.tag = ForceUpdate;
            const updateQueue = fiber.updateQueue;
            if (updateQueue === null) {
    
    
            } else {
    
    
              const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
              const pending = sharedQueue.pending;
              if (pending === null) {
    
    
                update.next = update;
              } else {
    
    
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
    
    
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );
          list.lanes = mergeLanes(list.lanes, renderLanes);
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
    
    
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (fiber.tag === DehydratedFragment) {
    
    
      //... 省略
    } else {
    
    
      nextFiber = fiber.child;
    }
    // 深度优先遍历找到下一个节点
    if (nextFiber !== null) {
    
    
      nextFiber.return = fiber;
    } else {
    
    
      nextFiber = fiber;
      while (nextFiber !== null) {
    
    
        if (nextFiber === workInProgress) {
    
    
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
    
    
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        nextFiber = nextFiber.return;
      }
    }
    fiber = nextFiber;
  }
}

消费 Context

上文我们提到了对 <Context.Provider> 节点的处理,那么之后我们来讲讲不同的消费方式的源码处理方式:

首先是 Context.Consumer 这种最常用的方式,它的处理还是在 beginWork 函数中,我们在上一部分讲到了, Consumer 指向 context 本身,所以就是 REACT_CONTEXT_TYPE 类型字段,其生成 fiber 时会识别 REACT_CONTEXT_TYPE 类型然后添加 ContextConsumer tag ,当我们识别到这个 tag ,就会调用 updateContextConsumer 进行处理。

updateContextConsumer 中的逻辑是先通过 prepareToReadContext 和 readContext 获取最新的 context 的值,再把最新的值传入子组件进行更新操作:

function beginWork(current, workInProgress, renderLanes) {
    
    
  switch (workInProgress.tag) {
    
    
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes)
  }
}

function updateContextConsumer(current, workInProgress, renderLanes) {
    
    
  let context = workInProgress.type
  context = context._context
  const newProps = workInProgress.pendingProps
  const render = newProps.children
  // 准备读取 context
  prepareToReadContext(workInProgress, renderLanes)
  // 获取最新的 context
  const newValue = readContext(context)
  // 更新包裹的子组件
  let newChildren
  newChildren = render(newValue)
  reconcileChildren(current, workInProgress, newChildren, renderLanes)
  return workInProgress.child
}

prepareToReadContext 中把 currentlyRenderingFiber 设置为当前的节点,方便后续取用,如果当前节点没有 dependencies 链表,则初始化一个链表,这个链表用于我们挂载 context 元素。

而在 readContext 中,它收集组件依赖的所有不同的 context,则将 context 添加到 fiber.dependencies 链表中,之后返回我们的 context._currentValue 作为我们需要的值,这个生成的 dependencies 后续会在我们更新一个 context 时用到,我们在上面已经提到了

function prepareToReadContext(workInProgress, renderLanes) {
    
    
  currentlyRenderingFiber = workInProgress
  lastContextDependency = null
  lastFullyObservedContext = null   // 重置
  const dependencies = workInProgress.dependencies
  if (dependencies !== null) {
    
    
    const firstContext = dependencies.firstContext
    if (firstContext !== null) {
    
    
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
    
    
        markWorkInProgressReceivedUpdate()
      }
      dependencies.firstContext = null
    }
  }
}
export function readContext<T>(context: ReactContext<T>): T {
    
    
    
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    
    
      // 不是可以使用 context 的时机
  } else {
    
    
    const contextItem = {
    
    
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };
    if (lastContextDependency === null) {
    
    

      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
    
    
        lanes: NoLanes,
        firstContext: contextItem,
      };
    } else {
    
    
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

useContext

useContext 作为一个用于 function 组件的钩子,它的作用方式和上文的直接消费基本一致,只是在作用的位置变成了在 hooks 的相关函数中。我们可以看到 useContextOnMountOnUpdate 其实就是调用了 readContext 函数,也就是我们上文的函数:

const HooksDispatcherOnMount: Dispatcher = 
  useContext: readContext,
  //....
};

const HooksDispatcherOnUpdate: Dispatcher = {
    
    
  useContext: readContext,
  //....
};

销毁 context

最后我们把目光看到 commit 阶段,在这个阶段的 completeWork 函数中,我们调用了一个函数 popProvider ,这个函数和我们的之前的 pushProvider 相互呼应,抛出了栈中的一个元素:

function popProvider(providerFiber) {
    
    
  var currentValue = valueCursor.current;
  pop(valueCursor, providerFiber);
  var context = providerFiber.type._context;

  {
    
    
    context._currentValue = currentValue;
  }
}

function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
    
    
  if (index < 0) {
    
    
    return;
  }
  cursor.current = valueStack[index];
  valueStack[index] = null;
  index--;
}

现在我们来分析一下为什么要用这个栈来存储我们的 context:

假设我们有一段下文这样的代码,因为我们的 context 是跨层级的,如果我们在深度优先遍历的时候需要在组件中传递 context 的值,就需要就需要一层一层往下传递,这样就会出现额外的开销是不能接收的,因此我们用了一个全局的变量来记录它。但是 Provider 是可能会嵌套的,代码中也会有多个值不同的 Provider。

索性我们用的是深度优先搜索来遍历 Fiber 的,它是一个先进后出的递过程,所以我们可以用一个 stack 记录我们所有的 context ,当我们读取到一个 Provider 的时候,把数值放入栈中,这样它的孩子都能在运行过程中读取到这个值,如果其中嵌套了另一个 Provider,我们在 stack 中添加一位并且更新值,这样这个嵌套的 Provider 的孩子节点获取到的就是距离它最近的新的值,当这个 Provider 销毁的时候,我们从栈中抛出这个值,那么被上一级 Provider 包裹的其他子组件就会获取到 上一级 Provider 的值。

render() {
    
    
  return (
    <>
      <TestContext.Provider value={
    
    10}>
        <Test1 />
        <TestContext.ProviderProvider value={
    
    100}>
          <Test2 />
        </TestContext.Provider>
      </TestContext.Provider>
    </>
  )
}

上述的原理也说明了一点:为什么当 Context.Providervalue 值发生变化时,所有使用 Context 的组件会强制更新,因为其中可能涉及到嵌套 Provider 的情况,如果我们局部更新的话,就不能正常的更新我们的栈,我们需要销毁整个栈然后重新生成,才能保证其中的调用顺序不发生变化,因此我们需要重新渲染其包裹的所有子元素。

总结

以上就是 context 相关的内容了,我们来总结一下:

  • 在 react 中 ,使用 Context 需要创建 一个 context,在指定数据的组件中 提供 这个 context,然后在子组件中 消费 这个 context
  • 用户使用 createContext 创建了可以 context ,它初始化了数据、Consumer 和 Provider ,让他们指向同一个 ReactContext 对象来保证用户总是拿到了最新的 context ,ReactContext 的 _currentValue 属性上放着这个 context 的数据
  • 我们使用 $$typeof 来标识一个组件是 Consumer 或者 Provider,他们会被处理为 reactElement 对象,在生成 Fiber 的时候使用不同的 tag 来标识他们
  • 在 Provider 初始化的时候,beginWork 中我们会将我们 context 的值压入栈中
  • 而在 Consumer 初始化的时候,一个 Fiber 上依赖的所有 context 会被放入一个 dependencies 链表中,因为 Consumer 指向ReactContext 本身,所以我们直接通过 _currentValue 就可以拿到需要的对象
  • 当一个 context 更新后,Provider 会进行判定,如果值发生变化不可复用,会调用 propagateContextChange 递归遍历所有的孩子节点,节点中使用了这个 Provider 的会被标识为强制更新优先级,在之后过程中被更新
  • 当一个 Provider 处理完毕,在 commit 阶段,入栈的数值会被 pop 出去,然后对应 context 的值也会更新为栈中上一个节点的内容,这样做是为了保证在多次嵌套的 context 中,用户获得的始终是离它最近的的 Provider 提供的值
  • useContext 作为一个钩子,它本身只是为了适配 function 组件,它做的就是调用 Consumer 逻辑中的 readContext 函数来获取 context 的值

至此,我们一小节一小节的 React 源码教程已经更新完毕了,内容比较杂也花费了不少的时间来阅读和理解,之后会抽空写一篇总结和梳理性的文章作为这个教程的结束,也感谢读到这里的大家。

猜你喜欢

转载自blog.csdn.net/weixin_46463785/article/details/131260862