【React源码笔记15】- context原理

React | 2020-08-25 22:31:46 1459次 8次

context 仍然是分为类组件和函数组件的写法,类组件中又有新版和旧版的两种写法,本次忽略旧版的写法。


一、createContext 声明

声明的过程中会提供出来 Provider 和 Consumer,指定这两个的组件类型,和 forwardRef 类似。

export function createContext<T>(
  defaultValue: T,
  calculateChangedBits
): ReactContext<T> {
  ...

  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    // 这个是一个函数,返回值要求是一个bit,为了跳过某些更新的,下面有说明
    _calculateChangedBits: calculateChangedBits,
    // 最多两个渲染器  _currentValue和_currentValue2是一样的
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    // 并发渲染器数量
    _threadCount: 0,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),
  };

  // 返回的 组件类型和 ref 一致 type上挂载的是这个对象
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  ...
  // 这里等到后面自然会明白为何如此设计
  context.Consumer = context;
  ...
  return context;
}


二、消费者

大致过程如下:

                    beginWork
                         |
                    updateContextConsumer
                         |
                        prepareToReadContext
                         |
                        readContext ---> dependencies: { firstContext }
                         |
                        ...
                        reconcileChildren

根据声明的组件创建出对应的组件类型,对应的 ContextConsumer 类型执行 updateContextConsumer 方法,这个过程创建依赖,生成最新的值,然后进入 diff 阶段:

function updateContextConsumer(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
) {
  // 拿到 context
  let context: ReactContext<any> = workInProgress.type;
  
  const newProps = workInProgress.pendingProps;
  // 这个方法就是 Consumer 组件中的那个函数
  const render = newProps.children;
  
  // 应该是高优先级的更新
  prepareToReadContext(workInProgress, renderExpirationTime);
  // 获取到最新值unstable_observedBits这个可以在消费者上手动传入
  const newValue = readContext(context, newProps.unstable_observedBits);
  
  let newChildren;
  // 拿到最新的组件,进行下一步 diff 更新
  newChildren = render(newValue);
  
  // 进入主流程 
  reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
  return workInProgress.child;
}

最重要的就是 readContext 这一步了:

export function readContext<T>(
  context: ReactContext<T>,
  observedBits: void | number | boolean,
): T {
  ...
    ...
    let contextItem = {
      context: ((context: any): ReactContext<mixed>),
      // 这个 如果开发者传入了这个属性则使用 否则就是使用最大值
      observedBits: resolvedObservedBits,
      next: null,
    };

    if (lastContextDependency === null) {
      
      lastContextDependency = contextItem;
      // 这个地方 提供者那里会需要  就是给 workInProgress 挂载
      currentlyRenderingFiber.dependencies = {
        expirationTime: NoWork,
        firstContext: contextItem,
        responders: null,
      };
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  // 返回最新的值 _currentValue2 生产者和消费者共享
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}

这里面隐藏着一个功能点--observedBits比如一个生产者同时服务了两个消费者,生产者有饮料和主食两个数据,假设消费者1仅需要饮料,消费者2仅需要主食,那么生产者如果只更新了饮料数据,是不需要通知消费者2的,observedBits 就是来做这个优化的,这个优化的过程在 Provider 中体现。举个例子:

const Context = React.createContext({food: 0, drink: 0}, (oldValue, newValue) => {
    let result = 0;
    if (oldValue.drink !== newValue.drink) {
      result |= 0b01;
    }
    if (oldValue.food !== newValue.food) {
      result |= 0b10;
    }
    return result;
  });

消费者1--0b01:

<Context.Consumer unstable_observedBits = {0b01}>
    ...
</Context.Consumer>

消费者2--0b10:

<Context.Consumer unstable_observedBits = {0b10}>
    ...
</Context.Consumer>

原理通过 unstable_observedBits & result 位运算不为 0 则证明当前消费者可以更新,但是这里的逻辑需要开发者控制好。


三、生产者

大致过程如下:

                    beginWork
                         |
                    updateContextProvider
                         |
                        pushProvider  ----> push
                         |
                        propagateContextChange  
                         |
                        ...
                        reconcileChildren

根据声明的组件创建出对应的组件类型,对应的 ContextProvider 类型执行 updateContextProvider 方法,这个过程创建依赖,生成最新的值,然后进入 diff 阶段。

function updateContextProvider(...) {
  // 拿到 context
  const providerType: ReactProviderType<any> = workInProgress.type;
  const context: ReactContext<any> = providerType._context;

  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  // 拿到属性上传入的vaule值
  const newValue = newProps.value;
  
  // 赋值 context._currentValue = nextValue;
  pushProvider(workInProgress, newValue);
  
  // 更新时
  if (oldProps !== null) {
    const oldValue = oldProps.value;
    // 这里的changedBits 是createContext第二个函数参数返回值 一般没有更新会返回 0
    const changedBits = calculateChangedBits(context, newValue, oldValue);
    if (changedBits === 0) {
      // 没有更新 bailoutOnAlreadyFinishedWork 跳过
    } else {
      // 让 消费者 更新时间刷新 方便更新
      propagateContextChange(...);
    }
  }
  // 进入主流程 开始diff
  reconcileChildren...
}

这里面更新时有一个初步的优化判断,但并不是第二部分描述的那个。初始加载时通过 pushProvider 挂载了初始值,所以消费者是可以直接使用的。

在 propagateContextChange,这个过程会找到所有 Consumer 节点,其他的则忽略,所以可以跨组件实现数据传递,找到具体的消费者之后,接着要做的就是从当前消费者节点逐步往上更新父节点的过期时间(scheduleWorkOnParentPath),再继续寻找其他的消费者,这样等待流程执行时根据过期时间会触发 render,没有变化的部分会被 diff 掉。

export function propagateContextChange(
  workInProgress: Fiber,
  context: ReactContext<mixed>,
  changedBits: number,
  renderExpirationTime: ExpirationTime,
): void {
  let fiber = workInProgress.child;
  ...
  while (fiber !== null) {
    let nextFiber;

    // 消费者才会有
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        // 上面提到的优化点在这里体现
        //  0001 & 0001 == 0001
        //  unstable_observedBits & result
        if (
          dependency.context === context &&
          (dependency.observedBits & changedBits) !== 0
        ) {
          ...
          scheduleWorkOnParentPath(fiber.return, renderExpirationTime);
          ...
          break;
        }
        // 下一个依赖
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
     ...
    } else {
      // 继续往下寻找
      nextFiber = fiber.child;
    }

    ...
    // 直到没有子节点 再遍历兄弟,如果没有兄弟的话一直回溯父节点为 wip 时会终止(nextFiber = null)
    fiber = nextFiber;
  }
}

context 生产者的更新会引起消费者的更新就是因为通过控制消费者的过期时间,所以本地的调度更新会触发所有消费者更新,就算某些组件设置了 shouldComponentUpdate false,也不会阻止其下方子节点更新。

由于生产者和消费者公用一个 context,所以通过 _currentValue 属性取值的数据是相通的。


四、useContext

这个方法就是就是直接调用 readContext 返回最新值。

8人赞

分享到: