React源码解析之Commit第二子阶段「mutation」(中)

1,533 阅读14分钟

前言
上篇文章中,我们讲了mutation子阶段的插入(Placement)操作,接下来我们讲更新(Update)和删除(Deletion)操作:

      //替换并更新该节点是Placement和Update的结合,就不讲了
      case PlacementAndUpdate: {
        // Placement
        //针对该节点及子节点进行插入操作
        commitPlacement(nextEffect);
        // Clear the "placement" from effect tag so that we know that this is
        // inserted, before any life-cycles like componentDidMount gets called.
        nextEffect.effectTag &= ~Placement;

        // Update
        const current = nextEffect.alternate;
        //对 DOM 节点上的属性进行更新
        commitWork(current, nextEffect);
        break;
      }
      //更新节点
      //旧节点->新节点
      case Update: {
        const current = nextEffect.alternate;
        //对 DOM 节点上的属性进行更新
        commitWork(current, nextEffect);
        break;
      }
      case Deletion: {
        //删除节点
        commitDeletion(nextEffect);
        break;
      }

一、commitWork()
作用:
DOM节点上的属性进行更新

源码:

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  //因为是执行 DOM 操作,所以supportsMutation为 true,下面这一段不看
  if (!supportsMutation) {
    //删除了本情况代码
  }

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // Note: We currently never use MountMutation, but useLayout uses
      // UnmountMutation.

      //循环 FunctionComponent 上的 effect 链,
      //根据hooks 上每个 effect 上的 effectTag,执行destroy/create 操作(类似于 componentDidMount/componentWillUnmount)
      //详情请看:[React源码解析之Commit第一子阶段「before mutation」](https://mp.weixin.qq.com/s/YtgEVlZz1i5Yp87HrGrgRA)中的「三、commitHookEffectList()」
      commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
      return;
    }
    case ClassComponent: {
      return;
    }
    //DOM 节点的话
    case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      if (instance != null) {
        // Commit the work prepared earlier.
        //待更新的属性
        const newProps = finishedWork.memoizedProps;
        // For hydration we reuse the update path but we treat the oldProps
        // as the newProps. The updatePayload will contain the real change in
        // this case.
        //旧的属性
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
        // TODO: Type the updateQueue to be specific to host components.
        //需要更新的属性的集合
        //比如:['style',{height:14},'__html',xxxx,...]
        //关于updatePayload,请看:
        // [React源码解析之HostComponent的更新(上)](https://juejin.cn/post/6844904079156592647)中的「四、diffProperties」
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        finishedWork.updateQueue = null;
        //进行节点的更新
        if (updatePayload !== null) {
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    }
    case HostText: {
      invariant(
        finishedWork.stateNode !== null,
        'This should have a text node initialized. This error is likely ' +
          'caused by a bug in React. Please file an issue.',
      );
      const textInstance: TextInstance = finishedWork.stateNode;
      const newText: string = finishedWork.memoizedProps;
      // For hydration we reuse the update path but we treat the oldProps
      // as the newProps. The updatePayload will contain the real change in
      // this case.
      const oldText: string =
        current !== null ? current.memoizedProps : newText;
      //源码即:textInstance.nodeValue = newText;
      commitTextUpdate(textInstance, oldText, newText);
      return;
    }
    case HostRoot: {
      return;
    }
    case Profiler: {
      return;
    }
    case SuspenseComponent: {
      commitSuspenseComponent(finishedWork);
      attachSuspenseRetryListeners(finishedWork);
      return;
    }
    case SuspenseListComponent: {
      attachSuspenseRetryListeners(finishedWork);
      return;
    }
    case IncompleteClassComponent: {
      return;
    }
    case EventComponent: {
      return;
    }
    default: {
      invariant(
        false,
        'This unit of work tag should not have side-effects. This error is ' +
          'likely caused by a bug in React. Please file an issue.',
      );
    }
  }
}

解析:
(1) 因为是执行DOM操作,所以supportsMutationtrue,下面这一段不看:

  if (!supportsMutation) {
    //删除了本情况代码
  }

(2) 主体逻辑是根据目标fibertag类型,进行不同的操作:
① 如果tag是函数组件FunctionComponent的话,则执行commitHookEffectList()方法,作用是:

循环FunctionComponent上的effect链,根据hooks上每个effect上的effectTag,执行destroy/create操作(类似于componentDidMount/componentWillUnmount

关于commitHookEffectList()的源码,请看:
React源码解析之Commit第一子阶段「before mutation」中的三、commitHookEffectList()

② 如果tag是DOM节点HostComponent的话,则获取要更新的属性newProps、旧属性oldProps和要更新的属性集合updatePayload,并执行commitUpdate(),进行更新

补充:
关于updatePayload更新队列是如何生成的,请看:
React源码解析之HostComponent的更新(上)中的四、diffProperties

③ 如果tagtext文本节点HostText的话,则比较简单了,执行commitTextUpdate(),源码就是替换文本值:

export function commitTextUpdate(
  textInstance: TextInstance,
  oldText: string,
  newText: string,
)
:
 void {
  textInstance.nodeValue = newText;
}

接下来,我们就看下DOM节点的更新—commitUpdate()方法

二、commitUpdate()
作用:
进行DOM节点的更新

源码:

export function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void 
{
  // Update the props handle so that we know which props are the ones with
  // with current event handlers.

  //挂载属性:node[internalEventHandlersKey] = props;
  updateFiberProps(domElement, newProps);
  // Apply the diff to the DOM node.
  //更新 DOM 属性
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

解析:
(1) 执行updateFiberProps(),将待更新的属性挂载到fiber对象的internalEventHandlersKey属性上

updateFiberProps()的源码如下:

const randomKey = Math.random().toString(36).slice(2)
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey

export function updateFiberProps(node, props{
  node[internalEventHandlersKey] = props;
}

(2) 执行updateProperties(),更新DOM属性

三、updateProperties()
作用:
diff prop操作,找出DOM节点上属性的不同,以更新

源码:

// Apply the diff.
//diff prop,找出DOM 节点上属性的不同,以更新
export function updateProperties(
  domElement: Element,
  updatePayload: Array<any>,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
): void {
  // Update checked *before* name.
  // In the middle of an update, it is possible to have multiple checked.
  // When a checked radio tries to change name, browser makes another radio's checked false.
  //如果是 radio 标签的话
  if (
    tag === 'input' &&
    nextRawProps.type === 'radio' &&
    nextRawProps.name != null
  ) {
    //单选按钮的相关操作,可不看
    ReactDOMInputUpdateChecked(domElement, nextRawProps);
  }
  //判断是否是自定义的 DOM 标签,具体请看:
  //[React源码解析之HostComponent的更新(下)](https://mp.weixin.qq.com/s/aB8jRVFzJ6EkkIqPVF3r1Q)中的「八、setInitialProperties」

  //之前是否是自定义标签
  const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
  //待更新的是否是自定义标签
  const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
  // Apply the diff.
  updateDOMProperties(
    domElement,
    updatePayload,
    wasCustomComponentTag,
    isCustomComponentTag,
  );

  // TODO: Ensure that an update gets scheduled if any of the special props
  // changed.
  //特殊标签的特殊处理,可不看
  switch (tag) {
    case 'input':
      // Update the wrapper around inputs *after* updating props. This has to
      // happen after `updateDOMProperties`. Otherwise HTML5 input validations
      // raise warnings and prevent the new value from being assigned.
      ReactDOMInputUpdateWrapper(domElement, nextRawProps);
      break;
    case 'textarea':
      ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
      break;
    case 'select':
      // <select> value update needs to occur after <option> children
      // reconciliation
      ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
      break;
  }
}

解析:
(1) 一些特殊标签的特殊处理就不细说了

(2) 关于isCustomComponent(),判断是否是自定义的 DOM 标签的源码,请看:
React源码解析之HostComponent的更新(下)中的八、setInitialProperties

接下来重点看下updateDOMProperties(),也就是DOM节点属性更新的核心源码

四、updateDOMProperties()
作用:
进行DOM节点的更新

源码:

function updateDOMProperties(
  domElement: Element,
  updatePayload: Array<any>,
  wasCustomComponentTag: boolean,
  isCustomComponentTag: boolean,
)
void 
{
  // TODO: Handle wasCustomComponentTag
  //遍历更新队列,注意 i=i+2,因为 updatePayload 是这样的:['style',{height:14},'__html',xxxx,...]
  //关于updatePayload,请看:
  // [React源码解析之HostComponent的更新(上)](https://juejin.cn/post/6844904079156592647)中的「四、diffProperties」
  for (let i = 0; i < updatePayload.length; i += 2) {
    //要更新的属性
    const propKey = updatePayload[i];
    //要更新的值
    const propValue = updatePayload[i + 1];
    //要更新style 属性的话,则执行setValueForStyles
    if (propKey === STYLE) {
      // 设置 style 的值,请看:
      // [React源码解析之HostComponent的更新(下)](https://juejin.cn/post/6844904085313814536)中的「八、setInitialProperties」中的第八点
      setValueForStyles(domElement, propValue);
    }

    else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // 设置innerHTML属性,请看:
      // [React源码解析之HostComponent的更新(下)](https://juejin.cn/post/6844904085313814536)中的「八、setInitialProperties」中的第八点
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      //设置textContent属性,请看:
      // [React源码解析之HostComponent的更新(下)](https://juejin.cn/post/6844904085313814536)中的「八、setInitialProperties」中的第八点
      setTextContent(domElement, propValue);
    } else {
      //为DOM节点设置属性值,即 setAttribute
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}

解析:
逻辑也简单,遍历更新队列,对不同的属性,进行不同的操作
总共进行了 4 种情况的操作:
(1) 对style属性,执行setValueForStyles(),来设置style的值

关于setValueForStyles()的讲解·,请看:
React源码解析之HostComponent的更新(下)中的八、setInitialProperties中的第八点

(2) 对innerHTML属性,执行setInnerHTML(),来设置innerHTML的值

关于setInnerHTML()的讲解·,请看:
React源码解析之HostComponent的更新(下)中的「八、setInitialProperties」中的第八点

(3) 对children属性,即设置 DOM 标签内部的值,执行setTextContent(),来设置textContent属性

关于setTextContent()的讲解·,请看:
React源码解析之HostComponent的更新(下)中的「八、setInitialProperties」中的第八点

(4) 除此之外的情况,就是为DOM节点设置属性值的情况,比如className,则执行setValueForProperty(),也就是调用setAttribute方法,就不解析了,放下源码:

export function setValueForProperty(
  node: Element,
  name: string,
  value: mixed,
  isCustomComponentTag: boolean,
{
  const propertyInfo = getPropertyInfo(name);
  if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
    return;
  }
  if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
    value = null;
  }
  // If the prop isn't in the special list, treat it as a simple attribute.
  if (isCustomComponentTag || propertyInfo === null) {
    if (isAttributeNameSafe(name)) {
      const attributeName = name;
      if (value === null) {
        node.removeAttribute(attributeName);
      } else {
        node.setAttribute(attributeName, '' + (value: any));
      }
    }
    return;
  }
  const {mustUseProperty} = propertyInfo;
  if (mustUseProperty) {
    const {propertyName} = propertyInfo;
    if (value === null) {
      const {type} = propertyInfo;
      (node: any)[propertyName] = type === BOOLEAN ? false : '';
    } else {
      // Contrary to `setAttribute`, object properties are properly
      // `toString`ed by IE8/9.
      (node: any)[propertyName] = value;
    }
    return;
  }
  // The rest are treated as attributes with special cases.
  const {attributeName, attributeNamespace} = propertyInfo;
  if (value === null) {
    node.removeAttribute(attributeName);
  } else {
    const {type} = propertyInfo;
    let attributeValue;
    if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
      attributeValue = '';
    } else {
      // `setAttribute` with objects becomes only `[object]` in IE8/9,
      // ('' + value) makes it output the correct toString()-value.
      attributeValue = '' + (value: any);
      if (propertyInfo.sanitizeURL) {
        sanitizeURL(attributeValue);
      }
    }
    if (attributeNamespace) {
      node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
    } else {
      node.setAttribute(attributeName, attributeValue);
    }
  }
}

总结
① 文本节点,执行textInstance.nodeValue = newText;,来替换文本值
② DOM标签,遍历更新队列updatePayload(['style',{height:14},'__html',xxxx,...]),针对styleinnerHTMLchildrenattribute进行属性更新

GitHub
commitWork()
github.com/AttackXiaoJ…

commitUpdate()
github.com/AttackXiaoJ…

updateDOMProperties()
github.com/AttackXiaoJ…

setValueForProperty()
github.com/AttackXiaoJ…


(完)