React Context:跨组件数据共享

368 阅读11分钟

在React应用中,随着组件结构的复杂化,数据传递成为了一个不可忽视的问题。传统的props逐级传递方式在面对深层嵌套的组件结构时,显得繁琐且难以维护。为了解决这一问题,React引入了Context API,使得数据可以在组件树中跨层级共享,而无需逐级传递props。

一、React Context的基本概念

React Context是一个API,它允许我们在组件树中共享数据,而不必通过每个级别显式地传递props。通过Context,我们可以将数据传递到树中任意深度的组件,无论其祖先组件是否知道该数据。这使得在复杂组件结构中共享数据变得更加方便和高效。

二、React Context的创建与使用

(1)创建Context

使用React.createContext()方法创建一个Context对象。这个方法返回一个对象,该对象包含ProviderConsumer两个React组件。

import React from 'react';
 
const MyContext = React.createContext(defaultValue); // defaultValue是Context的默认值

(2)提供Context值

使用Provider组件包裹需要共享数据的组件树,并通过value属性传递数据。

<MyContext.Provider value={/* some value */}>
  {/* 子组件树 */}
</MyContext.Provider>

(3)消费Context值

有两种方式可以消费Context中的值:使用Consumer组件或使用useContextHook。

  • 使用Consumer组件:
<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>
  • 使用useContextHook(在函数组件中):
import React, { useContext } from 'react';

function MyComponent() {
  const value = useContext(MyContext);
  // 使用value进行渲染
}

三、React Context的完整示例

以下是一个完整的示例,展示了如何在React应用中使用Context来共享主题设置。

(1)创建Context

首先,我们创建一个名为ThemeContext的Context对象,用于共享主题设置。

// ThemeContext.js
import React from 'react';

const ThemeContext = React.createContext('light'); // 默认主题为'light'

export const Provider = ThemeContext.Provider;
export const Consumer = ThemeContext.Consumer;
export default ThemeContext; // 现在 ThemeContext 也是默认导出的

(2)提供Context值

然后,在应用的根组件中,我们使用ThemeContext.Provider来提供主题设置的值。

// App.js
import React, { Component } from 'react';
import { Provider } from './ThemeContext';
import Toolbar from './Toolbar';

class App extends Component {

  render() {
    const theme = 'dark'; // 当前主题为'dark'
    return (
      <Provider value={theme}>
        <Toolbar />
      </Provider>
    );
  }
}

export default App;

(3)消费Context值

最后,在需要访问主题设置的组件中,我们使用ConsumeruseContext来消费Context的值。

  • 使用Consumer组件:
// Toolbar.js
import React from 'react';
import { Consumer } from './ThemeContext';
import ThemeButton from './ThemeButton';

const Toolbar = () => (
  <div>
    <ThemeButton />
  </div>
);

export default Toolbar;

// ThemeButton.js
import React, { useContext } from 'react';

const ThemeButton = () => {

  return (
    <Consumer>
      {theme => <button className={theme}>{theme} Theme Button</button>}
    </Consumer>
  );
};

export default ThemeButton;
  • 使用useContextHook(假设ThemeButton是一个函数组件):
// ThemeButton.js
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';

const ThemeButton = () => {
  const theme = useContext(ThemeContext);

  return (
    <button className={theme}>{theme} Theme Button</button>
  );

};	 

export default ThemeButton;

在这个示例中,我们创建了一个ThemeContext来共享主题设置。在应用的根组件App中,我们使用Provider来提供主题设置的值。然后,在Toolbar组件中,我们使用Consumer(或useContext,如果ThemeButton是一个函数组件)来消费这个值,并将其传递给ThemeButton组件。这样,无论ThemeButton在组件树中的哪个位置,它都能够访问到由App组件提供的主题设置。

四、原理

1. Context对象的创建:

当调用React.createContext时,会创建一个Context对象。这个对象包含了一些关键属性,如_currentValue(用于存放当前Context的值)、Provider(用于提供Context的组件)和Consumer(用于消费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;
}
2. Provider提供Context:

Provider是一个特殊的React组件,它接收一个value属性,并将其提供给其所有子组件。在React的协调(reconciliation)过程中,当遇到Provider组件时,会执行一系列操作来更新Context的值。

在React框架中,$$typeof这一特殊字段扮演着举足轻重的角色,它作为React元素的标识符,在元素创建时即被赋予。同样的,这个字段也被用于标记Context的Provider元素,从而确保在Fiber节点生成的过程中能够得到正确的处理。

具体来说,在React的源码中,createFiberFromTypeAndProps函数负责根据传入的元素类型和属性来构建Fiber节点。在这个函数内部,会检查元素的$$typeof字段以确定其类型,并根据这个类型给Fiber节点的tag属性赋予相应的值。

当我们创建Context时,Provider组件会被赋予REACT_PROVIDER_TYPE这一特定类型,而Consumer则直接指向Context对象本身,并因此拥有REACT_CONTEXT_TYPE类型。这两个类型在JSX解析过程中至关重要,因为一旦遇到它们,React就能够准确地识别出Provider和Consumer,并为它们分别执行相应的处理逻辑。

源码在packages/react-reconciler/src/ReactFiber.old.js这个文件中:

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。然后,从这些props当中提取出value,也就是我们期望传入的值。

接着,会调用pushProvider函数,这个函数有着重要的作用,它会对context_currentValue进行修改,意味着会更新context的值。同时,pushProvider函数还会执行一个压栈操作。

在此之后,需要判定是否可以复用相关内容。要是不能复用的话,那就得通过propagateContextChange方法来对更新进行标记,以便后续能准确地处理相应的更新情况。

源码在packages/react-reconciler/src/ReactFiberBeginWork.old.js这个文件中:

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;

  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;

  const newValue = newProps.value;

  if (__DEV__) {
    if (!('value' in newProps)) {
      if (!hasWarnedAboutUsingNoValuePropOnContextProvider) {
        hasWarnedAboutUsingNoValuePropOnContextProvider = true;
        console.error(
          'The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?',
        );
      }
    }
    const providerPropTypes = workInProgress.type.propTypes;

    if (providerPropTypes) {
      checkPropTypes(providerPropTypes, newProps, 'prop', 'Context.Provider');
    }
  }

  pushProvider(workInProgress, newValue);

  if (oldProps !== null) {
    // 是更新
    const oldValue = oldProps.value;
    const changedBits = calculateChangedBits(context, newValue, oldValue);
    if (changedBits === 0) {
      // 可以复用
      if (
        oldProps.children === newProps.children &&
        !hasLegacyContextChanged()
      ) {
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes,
        );
      }
    } else {
      // 查找consumer消费组件,标记更新
      propagateContextChange(workInProgress, context, changedBits, renderLanes);
    }
  }
  // 继续遍历
  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

我们注意到,当需要更新context时,会调用propagateContextChange方法来标记更新。那这个方法的具体逻辑是怎样的呢?下面我们来分析一下这个函数。

此函数会对所有子代fiber进行深度优先遍历,接着寻找其中具有dependencies属性的子代。这个dependencies属性挂载了某个元素所依赖的全部context。然后对比dependencies中的context与当前Providercontext是否相同。若二者相同,就会创建一个更新,并设定高fiber的更新优先级,这种更新方式类似于调用this.forceUpdate所引发的更新。

源码在packages/react-reconciler/src/ReactFiberNewContext.old.js这个文件中:

export function propagateContextChange(
  workInProgress: Fiber,
  context: ReactContext<mixed>,
  changedBits: number,
  renderLanes: Lanes,
): void {
  let fiber = workInProgress.child;
  if (fiber !== null) {
    // Set the return pointer of the child to the work-in-progress fiber.
    fiber.return = workInProgress;
  }
  // 深度优先遍历整个 fiber 树
  while (fiber !== null) {
    let nextFiber;

    // Visit this fiber.
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        // 如果是同一个 context
        if (
          dependency.context === context &&
          (dependency.observedBits & changedBits) !== 0
        ) {
          // Match! Schedule an update on this fiber.

          if (fiber.tag === ClassComponent) {
            // Schedule a force update on the work-in-progress.
            const update = createUpdate(
              NoTimestamp,
              pickArbitraryLane(renderLanes),
            );
            update.tag = ForceUpdate;
            // TODO: Because we don't have a work-in-progress, this will add the
            // update to the current fiber, too, which means it will persist even if
            // this render is thrown away. Since it's a race condition, not sure it's
            // worth fixing.
            enqueueUpdate(fiber, update);
          }
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          scheduleWorkOnParentPath(fiber.return, renderLanes);

          // Mark the updated lanes on the list, too.
          list.lanes = mergeLanes(list.lanes, renderLanes);

          // Since we already found a match, we can stop traversing the
          // dependency list.
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      // Don't scan deeper if this is a matching provider
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (
      enableSuspenseServerRenderer &&
      fiber.tag === DehydratedFragment
    ) {
      // If a dehydrated suspense boundary is in this subtree, we don't know
      // if it will have any context consumers in it. The best we can do is
      // mark it as having updates.
      const parentSuspense = fiber.return;
      invariant(
        parentSuspense !== null,
        'We just came from a parent so we must have had a parent. This is a bug in React.',
      );
      parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
      const alternate = parentSuspense.alternate;
      if (alternate !== null) {
        alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
      }
      // This is intentionally passing this fiber as the parent
      // because we want to schedule this fiber as having work
      // on its children. We'll use the childLanes on
      // this fiber to indicate that a context has changed.
      scheduleWorkOnParentPath(parentSuspense, renderLanes);
      nextFiber = fiber.sibling;
    } else {
      // Traverse down.
      nextFiber = fiber.child;
    }
    // 深度优先遍历找到下一个节点
    if (nextFiber !== null) {
      // Set the return pointer of the child to the work-in-progress fiber.
      nextFiber.return = fiber;
    } else {
      // No child. Traverse to next sibling.
      nextFiber = fiber;
      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          // We're back to the root of this subtree. Exit.
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
          // Set the return pointer of the sibling to the work-in-progress fiber.
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        // No more siblings. Traverse up.
        nextFiber = nextFiber.return;
      }
    }
    fiber = nextFiber;
  }
}
3. Consumer消费Context:

首先,最常用的方式是通过Context.Consumer来处理,这一处理过程位于beginWork函数中,我们在上一部分已经提到过。Consumer指向context本身,其类型为REACT_CONTEXT_TYPE。在生成fiber时,会识别这种REACT_CONTEXT_TYPE类型,并添加ContextConsumer标签。当识别到这个标签后,就会调用updateContextConsumer来进行处理。

updateContextConsumer中,其逻辑是先利用prepareToReadContextreadContext获取context的最新值,然后将这个最新值传入子组件,从而完成更新操作。

源码在packages/react-reconciler/src/ReactFiberBeginWork.old.js这个文件中:

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

function updateContextConsumer(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  let context: ReactContext<any> = workInProgress.type;
  if (__DEV__) {
    if ((context: any)._context === undefined) {
      if (context !== context.Consumer) {
        if (!hasWarnedAboutUsingContextAsConsumer) {
          hasWarnedAboutUsingContextAsConsumer = true;
          console.error(
            'Rendering <Context> directly is not supported and will be removed in ' +
              'a future major release. Did you mean to render <Context.Consumer> instead?',
          );
        }
      }
    } else {
      context = (context: any)._context;
    }
  }
  const newProps = workInProgress.pendingProps;
  const render = newProps.children;

  if (__DEV__) {
    if (typeof render !== 'function') {
      console.error(
        'A context consumer was rendered with multiple children, or a child ' +
          "that isn't a function. A context consumer expects a single child " +
          'that is a function. If you did pass a function, make sure there ' +
          'is no trailing or leading whitespace around it.',
      );
    }
  }
  // 准备读取 context
  prepareToReadContext(workInProgress, renderLanes);
  // 获取最新的 context
  const newValue = readContext(context, newProps.unstable_observedBits);
  let newChildren;
  if (__DEV__) {
    ReactCurrentOwner.current = workInProgress;
    setIsRendering(true);
    newChildren = render(newValue);
    setIsRendering(false);
  } else {
    // 更新包裹的子组件
    newChildren = render(newValue);
  }
  workInProgress.flags |= PerformedWork;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

prepareToReadContext函数中,会将currentlyRenderingFiber设置为当前节点,这样便于后续操作时取用。若当前节点不存在dependencies链表,那么会对其进行初始化,该链表的作用是挂载context元素。

对于readContext函数,它会收集组件所依赖的所有不同的context,并将这些context添加到fiber.dependencies链表中。完成添加后,readContext函数会返回context._currentValue作为所需的值。这里生成的dependencies在后续更新context时会发挥作用,这一点我们在前面已经提及。

源码在packages/react-reconciler/src/ReactFiberNewContext.old.js这个文件中:

export function prepareToReadContext(
  workInProgress: Fiber,
  renderLanes: Lanes,
): void {
  currentlyRenderingFiber = workInProgress;
  lastContextDependency = null;
  lastContextWithAllBitsObserved = null;

  const dependencies = workInProgress.dependencies;
  if (dependencies !== null) {
    const firstContext = dependencies.firstContext;
    if (firstContext !== null) {
      if (includesSomeLane(dependencies.lanes, renderLanes)) {
        // Context list has a pending update. Mark that this fiber performed work.
        markWorkInProgressReceivedUpdate();
      }
      // Reset the work-in-progress list
      dependencies.firstContext = null;
    }
  }
}

export function readContext<T>(
  context: ReactContext<T>,
  observedBits: void | number | boolean,
): T {
  if (__DEV__) {
    // This warning would fire if you read context inside a Hook like useMemo.
    // Unlike the class check below, it's not enforced in production for perf.
    if (isDisallowedContextReadInDEV) {
      console.error(
        'Context can only be read while React is rendering. ' +
          'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
          'In function components, you can read it directly in the function body, but not ' +
          'inside Hooks like useReducer() or useMemo().',
      );
    }
  }

  if (lastContextWithAllBitsObserved === context) {
    // Nothing to do. We already observe everything in this context.
  } else if (observedBits === false || observedBits === 0) {
    // Do not observe any updates.
  } else {
    let resolvedObservedBits; // Avoid deopting on observable arguments or heterogeneous types.
    if (
      typeof observedBits !== 'number' ||
      observedBits === MAX_SIGNED_31_BIT_INT
    ) {
      // Observe all updates.
      lastContextWithAllBitsObserved = ((context: any): ReactContext<mixed>);
      resolvedObservedBits = MAX_SIGNED_31_BIT_INT;
    } else {
      resolvedObservedBits = observedBits;
    }

    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      observedBits: resolvedObservedBits,
      next: null,
    };

    if (lastContextDependency === null) {
      invariant(
        currentlyRenderingFiber !== null,
        'Context can only be read while React is rendering. ' +
          'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
          'In function components, you can read it directly in the function body, but not ' +
          'inside Hooks like useReducer() or useMemo().',
      );

      // This is the first dependency for this component. Create a new list.
      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
        responders: null,
      };
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
4. useContext

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

源码在packages/react-reconciler/src/ReactFiberHooks.old.js这个文件中:

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

const HooksDispatcherOnUpdate: Dispatcher = {
  useContext: readContext,
  //....
};
5. 销毁 context

最后,我们把关注点放到commit阶段。在这个阶段的completeWork函数里,会调用一个名为popProvider的函数,这个函数与之前提到的pushProvider函数是相互呼应的关系,它的主要作用是将栈中的一个元素抛出。

pushProvider是一个具有存储功能的函数,其工作原理基于栈的特性。具体操作如下:

  • 先将context._currentValue压入栈内。
  • 然后将context._currentValue更新为nextValue

popProviderpushProvider相对应,同样依赖栈的特性。其作用如下:

  • 弹出栈中的值。
  • 把该值还原至context._currentValue

源码在packages/react-reconciler/src/ReactFiberNewContext.old.js这个文件中:

export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
  const context: ReactContext<T> = providerFiber.type._context;

  if (isPrimaryRenderer) {
    push(valueCursor, context._currentValue, providerFiber);

    context._currentValue = nextValue;
    if (__DEV__) {
      if (
        context._currentRenderer !== undefined &&
        context._currentRenderer !== null &&
        context._currentRenderer !== rendererSigil
      ) {
        console.error(
          'Detected multiple renderers concurrently rendering the ' +
            'same context provider. This is currently unsupported.',
        );
      }
      context._currentRenderer = rendererSigil;
    }
  } else {
    push(valueCursor, context._currentValue2, providerFiber);

    context._currentValue2 = nextValue;
    if (__DEV__) {
      if (
        context._currentRenderer2 !== undefined &&
        context._currentRenderer2 !== null &&
        context._currentRenderer2 !== rendererSigil
      ) {
        console.error(
          'Detected multiple renderers concurrently rendering the ' +
            'same context provider. This is currently unsupported.',
        );
      }
      context._currentRenderer2 = rendererSigil;
    }
  }
}

export function popProvider(providerFiber: Fiber): void {
  const currentValue = valueCursor.current;

  pop(valueCursor, providerFiber);

  const context: ReactContext<any> = providerFiber.type._context;
  if (isPrimaryRenderer) {
    context._currentValue = currentValue;
  } else {
    context._currentValue2 = currentValue;
  }
}

总结

在 React 中,使用 Context 需经历三个主要步骤:创建 context、在指定数据的组件中提供该 context,然后在子组件中消费它。

(1)通过createContext创建 context,此操作会初始化数据、ConsumerProvider,并使它们都指向同一个ReactContext对象,以此确保用户获取到的总是最新的 context。ReactContext_currentValue属性用于存放 context 的数据。

(2)利用$$typeof来标记一个组件是Consumer还是Provider,它们会被处理成reactElement对象。在生成Fiber时,会使用不同的tag来区分它们。

(3)在Provider初始化阶段,beginWork函数会将 context 的值压入栈中。而对于Consumer初始化,一个Fiber所依赖的所有 context 会被放入dependencies链表。因为Consumer指向ReactContext本身,所以可直接通过_currentValue获取所需对象。

(4)当一个 context 更新后,Provider会进行判断,如果值改变且不可复用,会调用propagateContextChange递归遍历所有子节点,使用了此Provider的节点会被标记为强制更新优先级,后续便会更新。

(5)当Provider处理完成,在commit阶段,入栈的值会被弹出,相应 context 的值也会更新为栈中前一个节点的内容,这样能保证在多层嵌套的 context 环境下,用户获取到的值始终是离它最近的Provider所提供的值。

(6)useContext作为一个钩子,主要是为了适配函数组件,其功能就是调用Consumer逻辑中的readContext函数来获取 context 的值。

参考链接:

blog.csdn.net/weixin_4646… juejin.cn/post/703093…

blog.csdn.net/weixin_4482…