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

前言
上篇文章中,咱們講了mutation子階段的插入(Placement)操做,接下來咱們講更新(Update)和刪除(Deletion)操做:javascript

      //替換並更新該節點是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;
      }
複製代碼

1、commitWork()
做用:
DOM節點上的屬性進行更新html

源碼:java

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)中的「3、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.im/post/5e5c5e1051882549003d1fc7)中的「4、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,下面這一段不看:node

  if (!supportsMutation) {
    //刪除了本狀況代碼
  }
複製代碼

(2) 主體邏輯是根據目標fibertag類型,進行不一樣的操做:
① 若是tag是函數組件FunctionComponent的話,則執行commitHookEffectList()方法,做用是:react

循環FunctionComponent上的effect鏈,根據hooks上每一個effect上的effectTag,執行destroy/create操做(相似於componentDidMount/componentWillUnmountgit

關於commitHookEffectList()的源碼,請看:
React源碼解析之Commit第一子階段「before mutation」中的3、commitHookEffectList()github

② 若是tag是DOM節點HostComponent的話,則獲取要更新的屬性newProps、舊屬性oldProps和要更新的屬性集合updatePayload,並執行commitUpdate(),進行更新web

補充:
關於updatePayload更新隊列是如何生成的,請看:
React源碼解析之HostComponent的更新(上)中的4、diffProperties數組

③ 若是tagtext文本節點HostText的話,則比較簡單了,執行commitTextUpdate(),源碼就是替換文本值:app

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

複製代碼

接下來,咱們就看下DOM節點的更新—commitUpdate()方法

2、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屬性

3、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)中的「8、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的更新(下)中的8、setInitialProperties

接下來重點看下updateDOMProperties(),也就是DOM節點屬性更新的核心源碼

4、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.im/post/5e5c5e1051882549003d1fc7)中的「4、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.im/post/5e65f86f6fb9a07cdc600e09)中的「8、setInitialProperties」中的第八點
      setValueForStyles(domElement, propValue);
    }

    else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // 設置innerHTML屬性,請看:
      // [React源碼解析之HostComponent的更新(下)](https://juejin.im/post/5e65f86f6fb9a07cdc600e09)中的「8、setInitialProperties」中的第八點
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      //設置textContent屬性,請看:
      // [React源碼解析之HostComponent的更新(下)](https://juejin.im/post/5e65f86f6fb9a07cdc600e09)中的「8、setInitialProperties」中的第八點
      setTextContent(domElement, propValue);
    } else {
      //爲DOM節點設置屬性值,即 setAttribute
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}
複製代碼

解析:
邏輯也簡單,遍歷更新隊列,對不一樣的屬性,進行不一樣的操做
總共進行了 4 種狀況的操做:
(1) 對style屬性,執行setValueForStyles(),來設置style的值

關於setValueForStyles()的講解·,請看:
React源碼解析之HostComponent的更新(下)中的8、setInitialProperties中的第八點

(2) 對innerHTML屬性,執行setInnerHTML(),來設置innerHTML的值

關於setInnerHTML()的講解·,請看:
React源碼解析之HostComponent的更新(下)中的「8、setInitialProperties」中的第八點

(3) 對children屬性,即設置 DOM 標籤內部的值,執行setTextContent(),來設置textContent屬性

關於setTextContent()的講解·,請看:
React源碼解析之HostComponent的更新(下)中的「8、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…


(完)

相關文章
相關標籤/搜索