ReactContext怎么实现的?如何更新使用Context的子组件的?

755 阅读5分钟

React context

Created: February 9, 2022 10:52 AM Tag: React Updated: February 9, 2022 4:59 PM

源码版本V17.3,不讨论lazyContext、observedBits(react官方已移除)、Suspend、并发context。

默认你知道 fiber 架构。

Context的功能

Context 提供了一种在组件之间共享值的方式,而不必显式地通过组件树的逐层传递 props。

Context的原理

生产者、消费者模式。

  • 生产者:Provider
  • 消费者:使用 context 的组件。

Context数据结构

ReactContext

// Context数据结构
type ReactContext<T> = {
  $$typeof: Symbol | number,     
  // Context本身会作为Consumer(消费者)
   // 意味着,Context就是ReactElement
  Consumer: ReactContext<T>,
  // 提供者 ReactElement
  Provider: ReactProviderType<T>,
  // 值
  _currentValue: T,
  // 支持并发
  _currentValue2: T,
  // 支持并发
  _threadCount: number,
};

// Provider也是ReactElement
type ReactProviderType<T> = {
  $$typeof: Symbol | number,
  _context: ReactContext<T>,
};

Dependencies

使用 Context 时, fiber 会在 dependencies 上记录此依赖项,方便 context 变动时,更新 fiber 的值。

// 依赖
type Dependencies = {
  // 优先级
  lanes: Lanes,
  // 第一个context
  firstContext: ContextDependency<mixed> | null,
};

// context依赖
type ContextDependency<T> = {
  // context
  context: ReactContext<T>,
  // 下一个 ContextDependency
  next: ContextDependency<mixed> | null,
  // 值
  memoizedValue: T,
};

type Fiber ={
  // 依赖项(context、event)。
  dependencies: Dependencies | null,
}

创建Context

通过 React.createContext 创建 Context

Function createContext<T>(defaultValue: T): ReactContext<T> {
  // 上面有数据结构有
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    Provider: (null: any),
    Consumer: (null: any),
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
  };

   // Provider
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
   // Consumer 就是 context自己
  context.Consumer = context;

  return context;
}
  • Context 的值保存在 _currentValue
  • 创建 ProviderConsumer 2个 ReactElement 对象。

Consumer 实际已经很少使用了,现在使用 useContext

用法复习

const Context = React.createContext();

function App() {
    const [v, setV] = useState('')
    return (
        <Context.Provider value={v}>
            <Read />
        </Context.Provider>
    );
}

function Read() {
    const v = useContext(Context)
    return v
}

Provider Component — 提供value

来看组件源码。

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;
   // 取新context的value
  const newValue = newProps.value;
   
  pushProvider(workInProgress, context, newValue);

  if (enableLazyContextPropagation) {
    // lzay模式
  } else {
    if (oldProps !== null) {
      const oldValue = oldProps.value;
      // 用Object.is对比value。 (比全等更严格)
      if (is(oldValue, newValue)) {
         // value 且 children 没变化,不更新。
        if (
          oldProps.children === newProps.children &&
          !hasLegacyContextChanged()
        ) {
               // 性能优化,复用fiber,bailout
          return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderLanes,
          );
        }
      } else {
        // 通知所有依赖此context的fiber,进行更新。
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }

  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}
  • Probider 不仅会全等比较 value,还会对比children
    • 优化注意:children 用父组件传值来保证不变。value若是对象,函数保证引用不变。
  • value变动,会使所有依赖此context的组件,进行更新。

下面接着看安排更新的函数逻辑。

propagateContextChange

为子组件中,所有依赖此 Context 的组件安排一个更新。

// dfs 子组件fiber树,为依赖此组件的安排lanes,准备更新
function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {
  // 检查Provider的 子节点
  let fiber = workInProgress.child;
  if (fiber !== null) {
    fiber.return = workInProgress;
  }
    
  // dfs子树,为所有依赖此context的fiber, 安排更新
  // 注意: 若子组件的上层,将同一个Context 的 Provider 使用多次,只有最近的Provider影响子组件。
  while (fiber !== null) {
    let nextFiber;
    // context依赖
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;
      let dependency = list.firstContext;
      
      // 发现依赖此context,则安排fiber更新
      while (dependency !== null) {
        // 检测context是否匹配
        if (dependency.context === context) {
          // 匹配,为fiber调度更新
          if (fiber.tag === ClassComponent) {
            // Class组件
            const lane = pickArbitraryLane(renderLanes);
            // 创建update
            const update = createUpdate(NoTimestamp, lane);
            // 强制更新
            update.tag = ForceUpdate;
            
            const updateQueue = fiber.updateQueue;
            // update插入updateQueue,且构成环形链表
            if (updateQueue === null) {
              // 只有在fiber已经卸载的情况下才会发生。 此时无逻辑
            } 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;
            }
          }

          // 将rednerLanes进行merge到fiber.lanes
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
                  // alternate做备份
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          // 从fiber的父节点一直安排到lane Provider节点
          // 因为是dfs,所以向上回溯。
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );

          // dependencies设置lanes
          list.lanes = mergeLanes(list.lanes, renderLanes);
          
               // 安排更新结束。break
          break;
        }
        // 下一个context
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
        // dfs发现是Provider组件且还是此Context的,终止DFS
      // 这是为了:若子组件的上层,将同一个Context 的 Provider 使用多次,只有最近的Provider影响子组件。
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (
      enableSuspenseServerRenderer &&
      fiber.tag === DehydratedFragment
    ) {
      // 服务端渲染的逻辑,也是安排一个lanes任务
      const parentSuspense = fiber.return;
      parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
      const alternate = parentSuspense.alternate;
      if (alternate !== null) {
        alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
      }
      scheduleContextWorkOnParentPath(
        parentSuspense,
        renderLanes,
        workInProgress,
      );
      nextFiber = fiber.sibling;
    } else {
        // dfs 子节点
      nextFiber = fiber.child;
    }

    // 判断无child,有则继续
    // 无则 寻找sibling,parent节点。
    if (nextFiber !== null) {
      nextFiber.return = fiber;
    } else {
      // dfs寻找sibling,return(parent)节点
      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 会强制安排更新,无视PureMemoshouComponentUpdate等优化逻辑。
  • 若子组件的上层,将同一个ContextProvider 使用多次,只有最近的Provider影响子组件。

使用Context

实际ClassComponentFunctionComponent 都按顺序调用 prepareToReadContext(准备去读Context)和readContext(读Context)。

prepareToReadContext — 准备读取Context

读取 Context 前的准备工作。

function prepareToReadContext(
  workInProgress: Fiber,
  renderLanes: Lanes,
): void {
  // 全局变量记录当前fiber, 为readContext做准备
  currentlyRenderingFiber = workInProgress;
  lastContextDependency = null;
  lastFullyObservedContext = null;

  // fiber的context依赖链表
  const dependencies = workInProgress.dependencies;
  if (dependencies !== null) {
    if (enableLazyContextPropagation) {
      // Reset the work-in-progress list
      dependencies.firstContext = null;
    } else {
      const firstContext = dependencies.firstContext;
      if (firstContext !== null) {
        // dependencies标记
        if (includesSomeLane(dependencies.lanes, renderLanes)) {
          // Context有lanes,标记组件进行更新
          markWorkInProgressReceivedUpdate();
        }
        // 清空dependencies链表
        dependencies.firstContext = null;
      }
    }
  }
}
  • Context 有更新,标记组件进行更新。
  • 清空 fiber 依赖,因为会重新读。

readContext — 读取Context

fiber上记录Context依赖。

// useContext,实际调用了readContext
function readContext<T>(context: ReactContext<T>): T {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    // Nothing to do. We already observe everything in this context.
  } else {
    // 链表节点
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };
      // 将依赖Dependency构成一个链表,添加到fiber上
    if (lastContextDependency === null) {
      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
    } else {
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}
  • 使用 Context ,会记录在 Dependency 中。

ContextConsumer

其实这个现在已经很少用了,不推荐使用。 逻辑是一样的,prepareToReadContext 后,readContext ,将value传入render

function updateContextConsumer(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  let context: ReactContext<any> = workInProgress.type;
  
  const newProps = workInProgress.pendingProps;
  const render = newProps.children;

  prepareToReadContext(workInProgress, renderLanes);
  const newValue = readContext(context);

  let newChildren = render(newValue);

  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

总结 — Summary

  • Probider 组件优化,不仅会全等比较 value,还会对比children
    • 优化注意:children 用父组件传值来保证不变。value若是对象,函数保证引用不变。
  • Probidervalue变动,会使所有依赖此Context的子组件,进行更新。
    • 强制安排更新,无视PureMemoshouComponentUpdate等优化逻辑。
  • 若子组件的上层,将同一个ContextProvider 使用多次,只有最近的Provider影响子组件。
  • 使用 Context ,会记录fiberDependency 中。

优化请看如何减少Render次数?了解bailout逻辑。