最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Context源码浅析

    正文概述 掘金(ZHANGYU)   2021-03-06   536

    简述

    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一改变,所有使用了的节点都需要更新啊!


    起源地下载网 » React Context源码浅析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元