discreteUpdate 更新路径

832 阅读2分钟

React源码中如何实现受控组件

不同于React其他组件props的更新会经历schedule - render - commit流程。 对于input、textarea、select,React有一条单独的更新路径,这条路径触发的更新被称为discreteUpdate。 这条路径的工作流程如下:

  1. 先以非受控的形式更新表单DOM
  2. 以同步的优先级开启一次更新
  3. 更新后的value在commit阶段并不会像其他props一样作用于DOM
  4. 调用restoreStateOfTarget方法,比较DOM的实际value(即步骤1中的非受控value)与步骤3中更新的value,如果相同则退出,如果不同则用步骤3的value更新DOM

本文是React源码中如何实现受控组件 文章中,关于 更新后的value在commit阶段并不会像其他props一样作用于DOM 这句描述的探究展开。

探究阶段一

首先 propscommit 阶段作用于 DOM 的方法是 updateDOMProperties

这个方法执行于 commit 阶段的 mutation 阶段,也就是在 commitMutationEffects 方法执行范围内。描述一下这个路径:

commitMutationEffects --> commitWork(update) --> commitUpdate(case HostComponent) --> updateProperties --> updateDOMProperties

看下 updateDOMProperties 代码:

function updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag) {
  for (var i = 0; i < updatePayload.length; i += 2) {
    var propKey = updatePayload[i];
    var propValue = updatePayload[i + 1];

    if (propKey === STYLE) {
      setValueForStyles(domElement, propValue);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    } else {
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}

那 input 更新 value 后,不作用于 DOM,意味着不经过这个方法(updateDOMProperties)或 updatePayload 为空。从代码来看,mutation 阶段都会调用到这个方法,所以 updatePayload 值应为空数组。

updateProperties 调用的 updateDOMProperties 方法打上断点:

function updateProperties(domElement, updatePayload, tag, lastRawProps, nextRawProps) {
  // 忽略其他代码
    updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag); // TODO: Ensure that an update gets scheduled if any of the special props

}

会发现 input 更新时,该断点的 updatePayload 的确是空数组。

探究阶段二

那么,input 更新在什么时候进行了特殊处理呢?

我们梳理一下,当 input 发生更新,进入 render 阶段,会经历 2 个方法,beginWorkcompleteWork

其中 beginWork 对 input 这些 HostComponent 没有进行特殊处理,只是执行了 updateHostComponent 方法生成 Fiber 节点。

当经历 completeWork 时,对于 HostComponent 会执行 updateHostComponent 方法:

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
    // 忽略无关代码
    switch (workInProgress.tag) {
      case HostComponent: {
        if (current !== null && workInProgress.stateNode != null) {
          updateHostComponent(
            current,
            workInProgress,
            type,
            newProps,
            rootContainerInstance,
          );
        }
      }
    }
  }

updateHostComponent 方法里会对 updatePayload 进行赋值。这个 updatePayload 就是我们追寻的:

input 更新时,该断点的 updatePayload 的确是空数组updatePayload。看下 updateHostComponent 方法的代码:

updateHostComponent = function(
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    const oldProps = current.memoizedProps;
    if (oldProps === newProps) {
      return;
    }
  
    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();
  
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );
  
    workInProgress.updateQueue = (updatePayload: any);
  
    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

从代码可以看到,该方法的主要作用是对 updatePayload 赋值以及标记该节点是否需要更新。

看下 prepareUpdate 方法:

function prepareUpdate(
  domElement: Instance,
  type: string,
  oldProps: Props,
  newProps: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): null | Array<mixed> {
  return diffProperties(
    domElement,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
  );
}

function diffProperties(
  domElement: Element,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
  rootContainerElement: Element | Document,
): null | Array<mixed> {
  // 已忽略其他无关代码
  let updatePayload: null | Array<any> = null;

  let lastProps: Object;
  let nextProps: Object;
  switch (tag) {
    case 'input':
      lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
      nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
      updatePayload = [];
      break;
    default:
      lastProps = lastRawProps;
      nextProps = nextRawProps;
      if (
        typeof lastProps.onClick !== 'function' &&
        typeof nextProps.onClick === 'function'
      ) {
        // TODO: This cast may not be sound for SVG, MathML or custom elements.
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      break;
  }
  return updatePayload;
}

prepareUpdate 返回的是 diffProperties 方法的执行结果,而在 diffProperties 方法内可以看到对 input 元素的特殊处理:updatePayload = []

这里应该明白:更新后的value在commit阶段并不会像其他props一样作用于DOM 的原因了。

总结

从头梳理一下:

  1. input 输入新内容,触发了更新;
  2. beginWork 生成了新的 input 节点;
  3. completeWorkHostComponent 进行处理(updateHostComponent),经过 diffProperties 方法执行后返回对应的 updatePayload 值。input 类型的 updatePayload 的值是空数组;
  4. commitmutation 阶段,对 DOM 进行更新,调用 updateDOMProperties,此时 input 类型的元素节点的 updatePayload 为空数组,所以不对它进行任何的 DOM 操作;

BTW

也大概记录一下非 input ,textarea,select 元素的更新路径吧:

  1. beginWork 生成 Fiber 节点
  2. completeWork 经历 diffProperties 方法执行后获得 updatePayload ,在 updateHostComponent 方法里根据 updatePaylaod 的值,标记是否需要更新
  3. commitmutation 阶段进行 DOM 更新updateDOMProperties

参考

[React源码中如何实现受控组件] mp.weixin.qq.com/s/Im8c0ZwZt…

[updateProperties] github.com/facebook/re…

[updateDOMProperties] github.com/facebook/re…

[completeWork / updateHostComponent] github.com/facebook/re…

[prepareUpdate / diffProperties] github.com/facebook/re…