zhangyu1818.

Tag »React

React Context源码浅析

2021年 3月5日在Github上查看

简述

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.js
const 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);
}

总结

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

  • 当Fiber节点为ContextProvider时,会将旧值压入栈,并为Context赋予新值,当有更新时,会遍历子级节点,找到有依赖关系的Fiber节点,标识它们需要更新。

  • 当Fiber节点需要使用Context时,会先调用prepareToReadContext方法来设置全局变量,读取Context需要调用readContext方法,该方法同时会记录此节点与Context的依赖关系。

  • 类组件和函数组件调用Context的逻辑实际上和ContextConsumer是一样的。

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