React16源码: React中的updateHostComponent的源码实现

updateHostComponent

1 )概述

  • completeWork 阶段的 HostComponent 处理,继续前文所述
  • 在更新的逻辑里面,调用了 updateHostComponent
  • 进行前后props对应的dom的attributes变化的对比情况
  • 这个方法也是根据不同环境来定义的,我们这里只专注于 react-dom 环境

2 )源码

定位到 packages/react-reconciler/src/ReactFiberCompleteWork.js#L586

这里是 updateHostComponent 调用的地方

updateHostComponent(
  current,
  workInProgress,
  type,
  newProps,
  rootContainerInstance,
);

if (current.ref !== workInProgress.ref) {
  markRef(workInProgress);
}

接着,进入其定义

updateHostComponent = function(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  rootContainerInstance: Container,
) {
  // If we have an alternate, that means this is an update and we need to
  // schedule a side-effect to do the updates.
  const oldProps = current.memoizedProps;
  // 全等对象,没有变化过,直接 return
  if (oldProps === newProps) {
    // In mutation mode, this is sufficient for a bailout because
    // we won't touch this node even if children changed.
    return;
  }

  // If we get updated because one of our children updated, we don't
  // have newProps so we'll have to reuse them.
  // TODO: Split the update API as separate for the props vs. children.
  // Even better would be if children weren't special cased at all tho.
  // 获取 dom 和 context
  const instance: Instance = workInProgress.stateNode;
  const currentHostContext = getHostContext();
  // TODO: Experiencing an error where oldProps is null. Suggests a host
  // component is hitting the resume path. Figure out why. Possibly
  // related to `hidden`.
  // 注意这里,
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext,
  );
  // TODO: Type this specific to this type of component.
  workInProgress.updateQueue = (updatePayload: any);
  // If the update payload indicates that there is a change or if there
  // is a new ref we mark this as an update. All the work is done in commitWork.
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};
  • 进入 prepareUpdate
    // 忽略 DEV 判断,这里直接 return 了 diffProperties 的调用
    export function prepareUpdate(
      domElement: Instance,
      type: string,
      oldProps: Props,
      newProps: Props,
      rootContainerInstance: Container,
      hostContext: HostContext,
    ): null | Array<mixed> {
      if (__DEV__) {
        const hostContextDev = ((hostContext: any): HostContextDev);
        if (
          typeof newProps.children !== typeof oldProps.children &&
          (typeof newProps.children === 'string' ||
            typeof newProps.children === 'number')
        ) {
          const string = '' + newProps.children;
          const ownAncestorInfo = updatedAncestorInfo(
            hostContextDev.ancestorInfo,
            type,
          );
          validateDOMNesting(null, string, ownAncestorInfo);
        }
      }
      return diffProperties(
        domElement,
        type,
        oldProps,
        newProps,
        rootContainerInstance,
      );
    }
    
    • 进入 diffProperties
      // 这个方法和前文中的 setInitialProperties 有一定的类似, 根据不同的节点做不同的操作
      // Calculate the diff between the two objects.
      export function diffProperties(
        domElement: Element,
        tag: string,
        lastRawProps: Object,
        nextRawProps: Object,
        rootContainerElement: Element | Document,
      ): null | Array<mixed> {
        if (__DEV__) {
          validatePropertiesInDevelopment(tag, nextRawProps);
        }
      
        let updatePayload: null | Array<any> = null;
      
        let lastProps: Object;
        let nextProps: Object;
        switch (tag) {
          case 'input':
            lastProps = ReactDOMInput.getHostProps(domElement, lastRawProps);
            nextProps = ReactDOMInput.getHostProps(domElement, nextRawProps);
            updatePayload = [];
            break;
          case 'option':
            lastProps = ReactDOMOption.getHostProps(domElement, lastRawProps);
            nextProps = ReactDOMOption.getHostProps(domElement, nextRawProps);
            updatePayload = [];
            break;
          case 'select':
            lastProps = ReactDOMSelect.getHostProps(domElement, lastRawProps);
            nextProps = ReactDOMSelect.getHostProps(domElement, nextRawProps);
            updatePayload = [];
            break;
          case 'textarea':
            lastProps = ReactDOMTextarea.getHostProps(domElement, lastRawProps);
            nextProps = ReactDOMTextarea.getHostProps(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;
        }
      
        assertValidProps(tag, nextProps);
      
        let propKey;
        let styleName;
        let styleUpdates = null;
        // 那么接下去会有两个循环
        // 第一个循环是循环 lastProps, 就是上一次渲染的结果产生的props
        // 这是第一次大循环: 找到在旧的props上有的属性,在新的props上没有的,也就是找到前后对比中删除的props
      
        for (propKey in lastProps) {
          // 对 新旧 props 进行循环判断: 新props有key 或 旧props没有key 或 旧props为null 则跳过此key
          if (
            nextProps.hasOwnProperty(propKey) ||
            !lastProps.hasOwnProperty(propKey) ||
            lastProps[propKey] == null
          ) {
            // 符合这个条件,不是被删除的属性,不会加到最后的 updatePayload
            continue;
          }
          // 对符合被删除属性来说,进行删除 style 的操作
          if (propKey === STYLE) {
            const lastStyle = lastProps[propKey];
            for (styleName in lastStyle) {
              if (lastStyle.hasOwnProperty(styleName)) {
                if (!styleUpdates) {
                  styleUpdates = {};
                }
                styleUpdates[styleName] = '';
              }
            }
          // 后面的其他判断,都没有做什么事情,可以跳过了
          } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
            // Noop. This is handled by the clear text mechanism.
          } else if (
            propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
            propKey === SUPPRESS_HYDRATION_WARNING
          ) {
            // Noop
          } else if (propKey === AUTOFOCUS) {
            // Noop. It doesn't work on updates anyway.
          } else if (registrationNameModules.hasOwnProperty(propKey)) {
            // This is a special case. If any listener updates we need to ensure
            // that the "current" fiber pointer gets updated so we need a commit
            // to update this element.
            if (!updatePayload) {
              updatePayload = [];
            }
          } else {
            // For all other deleted properties we add it to the queue. We use
            // the whitelist in the commit phase instead.
            // 注意这里,加入 updatePayload 的方式是 一次加入 key, value 这2个
            (updatePayload = updatePayload || []).push(propKey, null);
          }
        }
        // 这是第二次大循环, nextProps 就是新的props
        for (propKey in nextProps) {
          // 处理 新旧 props
          const nextProp = nextProps[propKey];
          const lastProp = lastProps != null ? lastProps[propKey] : undefined;
          // 新props没有这个key(这个key在原型链上,不在自己),或 两个 prop 相同,或两个prop 都为 null
          if (
            !nextProps.hasOwnProperty(propKey) ||
            nextProp === lastProp ||
            (nextProp == null && lastProp == null)
          ) {
            // 符合上述条件跳过
            continue;
          }
          // 对于 style 类型的 props 的处理, 判断每个 style 的key是否有变化
          // 下面会有两个循环来处理
          if (propKey === STYLE) {
            if (__DEV__) {
              if (nextProp) {
                // Freeze the next style object so that we can assume it won't be
                // mutated. We have already warned for this in the past.
                Object.freeze(nextProp);
              }
            }
            if (lastProp) {
              // Unset styles on `lastProp` but not on `nextProp`.
              for (styleName in lastProp) {
                if (
                  lastProp.hasOwnProperty(styleName) &&
                  (!nextProp || !nextProp.hasOwnProperty(styleName))
                ) {
                  if (!styleUpdates) {
                    styleUpdates = {};
                  }
                  styleUpdates[styleName] = '';
                }
              }
              // Update styles that changed since `lastProp`.
              for (styleName in nextProp) {
                if (
                  nextProp.hasOwnProperty(styleName) &&
                  lastProp[styleName] !== nextProp[styleName]
                ) {
                  if (!styleUpdates) {
                    styleUpdates = {};
                  }
                  styleUpdates[styleName] = nextProp[styleName];
                }
              }
            } else {
              // Relies on `updateStylesByID` not mutating `styleUpdates`.
              if (!styleUpdates) {
                if (!updatePayload) {
                  updatePayload = [];
                }
                updatePayload.push(propKey, styleUpdates);
              }
              styleUpdates = nextProp;
            }
          } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
            const nextHtml = nextProp ? nextProp[HTML] : undefined;
            const lastHtml = lastProp ? lastProp[HTML] : undefined;
            if (nextHtml != null) {
              // 两个html不相等则加入
              if (lastHtml !== nextHtml) {
                (updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
              }
            } else {
              // TODO: It might be too late to clear this if we have children
              // inserted already.
            }
          } else if (propKey === CHILDREN) {
            // 对于 children 的处理
            if (
              lastProp !== nextProp &&
              (typeof nextProp === 'string' || typeof nextProp === 'number')
            ) {
              // 不同则加入
              (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
            }
          } else if (
            propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
            propKey === SUPPRESS_HYDRATION_WARNING
          ) {
            // Noop
          } else if (registrationNameModules.hasOwnProperty(propKey)) {
            // 这里是事件绑定相关的处理
            if (nextProp != null) {
              // We eagerly listen to this even though we haven't committed yet.
              if (__DEV__ && typeof nextProp !== 'function') {
                warnForInvalidEventListener(propKey, nextProp);
              }
              ensureListeningTo(rootContainerElement, propKey);
            }
            if (!updatePayload && lastProp !== nextProp) {
              // This is a special case. If any listener updates we need to ensure
              // that the "current" props pointer gets updated so we need a commit
              // to update this element.
              updatePayload = [];
            }
          } else {
            // For any other property we always add it to the queue and then we
            // filter it out using the whitelist during the commit.
            // 上面都没找到,这个属性是新增属性,直接加入
            (updatePayload = updatePayload || []).push(propKey, nextProp);
          }
        }
        // 最终处理加入 styleUpdates
        if (styleUpdates) {
          (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
        }
        // 最终返回
        return updatePayload;
      }
      
    • diffProperties 里的 updatePayload 一开始是一个空的数组,并且最终被return
    • 对于 completeWork 来说,拿到的 updatePayload,就是 diffPropertiesprepareUpdate 返回过来的内容
    • 接下去,会有一个判断 if (updatePayload) {} 也就是说空数组也是符合这个判断条件的, 也会执行 markUpdate
    • 上述标记之后,在后续的 commit 的时候进行对应的操作
  • 以上是 updateHostComponent 的过程
    • 具体细节,参考代码里标注的中文文档和源码
    • vdom其实就是这些东西,通过props来进行 attributes 的判断和处理变化的过程
    • 主要是 整个 diffProperties 的处理