ZHANGYU.dev

October 14, 2023

React Context源码浅析

React14.2 min to read

简述

React中,有一个valueStack,是一个栈结构,其中会存入Context信息,在beginWork阶段,当Fiber节点为ContextProvider时,会将当前的Context的旧值压入栈,并赋予新值,当此Fiber节点执行到completeWork阶段时,会将旧值弹出,以保证Fiber节点之间的层级关系。

Context的值就存在Context对象本身的_currentValue字段,当Fiber节点读取Context值时,会直接从Context上获取值,同时会创建Fiber节点的dependencies并将Context信息存入,在Context值改变时,会从当前ContextProvider向下遍历,找到所有depenencies里与Context相同的Fiber节点,标识它们需要更新。Context值本身改变是不会触发更新的,依旧需要使用setState这类方法。

以下源码浅析的React版本为17.0.1,需要先了解Fiber树的构建流程。

valueStack

valueStack定义在ReactFiberStack.js文件中, valueStack存储了几种数据,并不是只存储Context的值。

export type StackCursor<T> = {|current: T|};const valueStack: Array<any> = [];let index = -1;function createCursor<T>(defaultValue: T): StackCursor<T> {  return {    current: defaultValue,  };}function isEmpty(): boolean {  return index === -1;}function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {  if (index < 0) {    return;  }  cursor.current = valueStack[index];  valueStack[index] = null;  index--;}function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {  index++;  valueStack[index] = cursor.current;  cursor.current = value;}

其中有一种数据的类型为StackCursor,该类型也定义在ReactFiberNewContext.js文件中,用来存储Context的新值,它的作用就是传递valueStack里的值。

// ReactFiberNewContext.jsconst valueCursor: StackCursor<mixed> = createCursor(null);

后文有关Context处理的方法都定义在这个文件里。

Context的创建开始看源码。

createContext

createContext方法实际是创建了一个对象,该对象会作为ReactElementtype,同时使用了$$typeof字段区分REACT_PROVIDER_TYPE类型和REACT_CONTEXT_TYPE类型。

export function createContext<T>(  defaultValue: T,  calculateChangedBits: ?(a: T, b: T) => number,): ReactContext<T> {  if (calculateChangedBits === undefined) {    calculateChangedBits = null;  }  const context: ReactContext<T> = {    $$typeof: REACT_CONTEXT_TYPE,    _calculateChangedBits: calculateChangedBits,    _currentValue: defaultValue, // context读取的值    _currentValue2: defaultValue,    _threadCount: 0,    Provider: (null: any), // Provider    Consumer: (null: any), // Consumer 为 context本身  };  context.Provider = {    $$typeof: REACT_PROVIDER_TYPE,    _context: context,  };  context.Consumer = context;  return context;}

当我们将ProviderJSX模式使用时,会创建对应的Fiber节点,也会进入beginWorkcompleteWork阶段。

通过createContext方法可以知道ProviderConsumer为一个对象,首先会进入Fiber节点的创建。

Fiber节点的创建

createFiberFromTypeAndProps方法会创建并返回Fiber节点,在这个方法里会判断Fiber节点的类型,ProviderConsumer都是对象,进入default判断后会以$$typeof来判断类型。

export function createFiberFromTypeAndProps(// ...): Fiber {  let fiberTag = IndeterminateComponent;  let resolvedType = type;  if (typeof type === 'function') {    // ...  } else if (typeof type === 'string') {    fiberTag = HostComponent;  } else {    getTag: switch (type) {      // ...      default: {        if (typeof type === 'object' && type !== null) {          switch (type.$$typeof) {            // tag为ContextProvider            case REACT_PROVIDER_TYPE:              fiberTag = ContextProvider;              break getTag;            // tag为ContextConsumer            case REACT_CONTEXT_TYPE:              fiberTag = ContextConsumer;              break getTag;              // ...          }        }      }    }  }  const fiber = createFiber(fiberTag, pendingProps, key, mode);  // ...}

beginWork阶段

beginWork阶段会以Fiber节点的tag判断进入哪一个方法,在Fiber节点创建的时候已经为ProviderConsumer设置了对应的tag

function beginWork(// ...): Fiber | null {    // ...    case ContextProvider:      return updateContextProvider(current, workInProgress, renderLanes);    case ContextConsumer:      return updateContextConsumer(current, workInProgress, renderLanes);    // ...}

updateContextProvider

ContextProvider类型会进入updateContextProvider方法。

function updateContextProvider(  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,) {  /*    type = {      $$typeof: REACT_PROVIDER_TYPE,      _context: context,    };   */  const providerType: ReactProviderType<any> = workInProgress.type;  // 取出context  const context: ReactContext<any> = providerType._context;  const newProps = workInProgress.pendingProps;  const oldProps = workInProgress.memoizedProps;  // Provider所传入的value值  const newValue = newProps.value;  // 旧值入栈,赋新值  pushProvider(workInProgress, newValue);  // Update时  if (oldProps !== null) {    const oldValue = oldProps.value;    // value值无变化返回0,有变化返回MAX_SIGNED_31_BIT_INT    const changedBits = calculateChangedBits(context, newValue, oldValue);    if (changedBits === 0) {      // context没有变化      if (        oldProps.children === newProps.children &&        !hasLegacyContextChanged()      ) {        return bailoutOnAlreadyFinishedWork(          current,          workInProgress,          renderLanes,        );      }    } else {      // context的值变化了,所有消费了context的组件需要发起更新      propagateContextChange(workInProgress, context, changedBits, renderLanes);    }  }  const newChildren = newProps.children;  reconcileChildren(current, workInProgress, newChildren, renderLanes);  return workInProgress.child;}
pushProvider

pushProvider方法是Context的值变化的核心,它会将旧的值压入valueStack,同时为Context赋新值。

export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {  const context: ReactContext<T> = providerFiber.type._context;  // context旧值入栈  push(valueCursor, context._currentValue, providerFiber); // Stack标题内的push方法  // context的值设为新的值  context._currentValue = nextValue;}
propagateContextChange

propagateContextChange方法在Context更新时使用,从当前Fiber节点开始遍历节点树,为使用了当前context的子节点设置优先级。

设置优先级的目的是为了子节点在进入beginWork阶段的时候不会进入bailout的复用流程。

export function propagateContextChange(  workInProgress: Fiber,  context: ReactContext<mixed>,  changedBits: number,  renderLanes: Lanes,): void {  let fiber = workInProgress.child;  if (fiber !== null) {    fiber.return = workInProgress;  }  while (fiber !== null) {    let nextFiber;    const list = fiber.dependencies;    if (list !== null) {      nextFiber = fiber.child;      let dependency = list.firstContext;      // 当前的子节点需要更新      while (dependency !== null) {        if (          dependency.context === context &&          (dependency.observedBits & changedBits) !== 0        ) {          // 匹配,从该Fiber节点发起更新          // 如果是类组件使用了context,则添加一个forceUpdate的Update          if (fiber.tag === ClassComponent) {            const update = createUpdate(              NoTimestamp,              // renderLanes中最高优先级的lane              pickArbitraryLane(renderLanes),            );            update.tag = ForceUpdate;            enqueueUpdate(fiber, update);          }          // 后续fiber节点的遍历会判断节点有更新          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);          const alternate = fiber.alternate;          if (alternate !== null) {            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);          }          // 设置lane标识有更新          scheduleWorkOnParentPath(fiber.return, renderLanes);          // 体现在prepareToReadContext方法里          list.lanes = mergeLanes(list.lanes, renderLanes);          break;        }        dependency = dependency.next;      }    } else if (fiber.tag === ContextProvider) {      // 子节点是ContextProvider返回null      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;    } else if (      enableSuspenseServerRenderer &&      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;  }}

updateContextConsumer

Consumer是使用Context值的一种最基础的方式,ContextConsumer类型会进入updateContextConsumer方法。

function updateContextConsumer(  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,) {  // Consumer的type就是context本身  let context: ReactContext<any> = workInProgress.type;  const newProps = workInProgress.pendingProps;  const render = newProps.children;  // 全局变量的处理  prepareToReadContext(workInProgress, renderLanes);  // 读取context的值  const newValue = readContext(context, newProps.unstable_observedBits);  // render props的方式来将context值提供给子组件  let newChildren = render(newValue);  reconcileChildren(current, workInProgress, newChildren, renderLanes);  return workInProgress.child;}

在这里与context相关的是prepareToReadContext方法和readContext方法。

prepareToReadContext

export function prepareToReadContext(  workInProgress: Fiber,  renderLanes: Lanes,): void {  // 把使用了context值的Fiber节点存在全局变量  currentlyRenderingFiber = workInProgress;  // 重置变量标识标识  lastContextDependency = null;  lastContextWithAllBitsObserved = null;  const dependencies = workInProgress.dependencies;  // context更新后才会进入判断  if (dependencies !== null) {    const firstContext = dependencies.firstContext;    if (firstContext !== null) {      if (includesSomeLane(dependencies.lanes, renderLanes)) {        // 设置didReceiveUpdate,函数组件内会根据这个判断        markWorkInProgressReceivedUpdate();      }      // 没懂      dependencies.firstContext = null;    }  }}

readContext

readContext方法不止在ContextConsumer会用到,使用了contextType的类组件和使用了useContext的函数组件都会使用(后文),该方法不仅会返回context的值,同时也记录了该Fiber节点使用了Context,后续Context改变会触发此节点的更新。

export function readContext<T>(  context: ReactContext<T>,  observedBits: void | number | boolean,): T {  // 这一片逻辑与更新有关,实际值返回只在最后一行代码  if (lastContextWithAllBitsObserved === context) {    // 已经observe了  } else if (observedBits === false || observedBits === 0) {    // 不更新  } else {    let resolvedObservedBits; // Avoid deopting on observable arguments or heterogeneous types.    // 默认情况observedBits等于MAX_SIGNED_31_BIT_INT    if (      typeof observedBits !== 'number' ||      observedBits === MAX_SIGNED_31_BIT_INT    ) {      // 标识Observed      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) {      // 该Fiber节点的第一个依赖      lastContextDependency = contextItem;      // 记录该Fiber节点使用的Context      currentlyRenderingFiber.dependencies = {        lanes: NoLanes,        firstContext: contextItem,        responders: null,      };    } else {      // 已有其他的context依赖记录,用next连接      lastContextDependency = lastContextDependency.next = contextItem;    }  }  return context._currentValue;}

completeWork阶段

completedWork阶段会将调用popProvider将当前valueStack栈中的旧值弹出并赋值给ContextProvider

function completeWork(// ...): Fiber | null {  // ...  case ContextProvider:  	// Pop provider fiber 	 popProvider(workInProgress); 	 return null;  // ...}

popProvider

export function popProvider(providerFiber: Fiber): void {  const currentValue = valueCursor.current;  pop(valueCursor, providerFiber);  const context: ReactContext<any> = providerFiber.type._context;  // 改变context的值为旧值  context._currentValue = currentValue;}

为什么会赋值为旧值呢?如以下情况。

const Context = React.createContext(-1);<Context.Provider value={0}>  <Context.Provider value={1}>  	<A/> // 1  </Context.Provider>  <B/> // 0</Context.Provider>

beginWork执行到value = 0ContextPrivder时,将默认值-1压入栈,同时赋予新值0,接下来执行value = 1ContextProvider,将旧值0压入栈,同时赋予新值1,这时候A组件读取的值为1

接下来执行completeWork阶段,当到value = 1ContextProvider时,将旧值0从栈弹出,同时赋予旧值。

接下来在B组件的beginWork阶段,读取的ContextProvider的值才会为正确的0,最后依次执行completeWork阶段,将ContextProvider值还原为默认值-1

为了保证这样的层级关系,所以需要保留旧值来还原。

子级如何判断有更新?

在Fiber树构建流程中,如果当前更新的renderLanes不包含WorkInProgresslane,就会进入bailoutOnAlreadyFinishedWork方法,就不会走更新流程了。

所以在propagateContextChange方法里,会对使用了Context对子级节点设置lane,确保不会进入bailoutOnAlreadyFinishedWork方法。

function beginWork(  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,): Fiber | null {  const updateLanes = workInProgress.lanes;  // renderLans不包含节点lane,就会进入bailout方法  if (!includesSomeLane(renderLanes, updateLanes)) {    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);  }      // update...}

类组件和函数组件使用Context

类组件使用方式是利用contextType,函数组件则是简单的useContext

类组件

类组件流程和ContextConsumer是一样的,同样先调用prepareToReadContext重置全局变量,在通过调用readContext获取context并添加依赖关系。

function updateClassComponent(// ...) {  // 重置全局变量  prepareToReadContext(workInProgress, renderLanes);    // 以下简化了代码...    // 类实例  const instance = workInProgress.stateNode;  const contextType = ctor.contextType;  if (typeof contextType === 'object' && contextType !== null) {    // 读取context挂在实例的context上    instance.context = readContext(contextType);  }  // ...}

函数组件

函数组件同样是需要先调用prepareToReadContext重置全局变量,再调用useContext来获取值。

function updateFunctionComponent(// ...) {  prepareToReadContext(workInProgress, renderLanes);  // 执行函数组件,来获取children,有使用useContext就会执行readContext  let nextChildren = renderWithHooks(			// ...  );  // 在prepareToReadContext判断后didReceiveUpdate为true,不会进入bailout  if (current !== null && !didReceiveUpdate) {    bailoutHooks(current, workInProgress, renderLanes);    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);  }  // ...}

useContext本质上就是readContext,和其他Hooks非常不一样。

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

总结

文章逻辑写的有点狗屁不通,难受啊。

在这里还发现一个有意思的东西,createContext的第二个参数calculateChangedBits,在文档上是没有使用的,看逻辑应该是和是否需要更新节点有关,原来并不是Context一改变,所有使用了的节点都需要更新啊!