React源碼解析之HostComponent的更新(下)

前言:
在上篇 React源碼解析之HostComponent的更新(上) 中,咱們講到了屢次渲染階段的更新,本篇咱們講第一次渲染階段的更新javascript

1、HostComponent(第一次渲染)
做用:
(1) 建立 DOM 實例
(2) 插入子節點
(3) 初始化事件監聽器php

源碼:css

     else {
        //若是是第一次渲染的話

        //若是沒有新 props 更新,可是執行到這裏的話,多是 React 內部出現了問題
        if (!newProps) {
          invariant(
            workInProgress.stateNode !== null,
            'We must have new props for new mounts. This error is likely ' +
              'caused by a bug in React. Please file an issue.',
          );
          // This can happen when we abort work.
          break;
        }
        //context 相關,暫時跳過
        const currentHostContext = getHostContext();
        // TODO: Move createInstance to beginWork and keep it on a context
        // "stack" as the parent. Then append children as we go in beginWork
        // or completeWork depending on we want to add then top->down or
        // bottom->up. Top->down is faster in IE11.
        //是否曾是服務端渲染
        let wasHydrated = popHydrationState(workInProgress);
        //若是是服務端渲染的話,暫時跳過
        if (wasHydrated) {
          //暫時刪除
        }
        //不是服務端渲染
        else {
           //建立 DOM 實例
           //一、建立 DOM 元素
           //二、建立指向 fiber 對象的屬性,方便從DOM 實例上獲取 fiber 對象
           //三、建立指向 props 的屬性,方便從 DOM 實例上獲取 props
          let instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          //插入子節點
          appendAllChildren(instance, workInProgress, falsefalse);

          // Certain renderers require commit-time effects for initial mount.
          // (eg DOM renderer supports auto-focus for certain elements).
          // Make sure such renderers get scheduled for later work.
          if (
            //初始化事件監聽
            //若是該節點可以自動聚焦的話
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            //添加 EffectTag,方便在 commit 階段 update
            markUpdate(workInProgress);
          }
          //將處理好的節點實例綁定到 stateNode 上
          workInProgress.stateNode = instance;
        }
        //若是 ref 引用不爲空的話
        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          //添加 Ref 的 EffectTag
          markRef(workInProgress);
        }
      }
複製代碼

解析:
(1) 執行createInstance(),建立該 fiber 對象對應的 DOM 對象
(2) 執行appendAllChildren(),插入全部子節點
(3) 執行finalizeInitialChildren(),初始化事件監聽,而且判斷該節點若是有autoFocus屬性併爲true時,執行markUpdate(),添加EffectTag,方便在commit階段update
(4) 最後將建立並初始化好的 DOM 對象綁定到fiber對象的stateNode屬性上
(5) 最後更新下RefEffectTag便可html

咱們先來看下createInstance()方法java

2、createInstance
做用:
建立DOM對象node

源碼:react

export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance 
{
  let parentNamespace: string;
  if (__DEV__) {
    //刪除了 dev 代碼
  } else {
    //肯定該節點的命名空間
    // 通常是HTML,http://www.w3.org/1999/xhtml
    //svg,爲 http://www.w3.org/2000/svg ,請參考:https://developer.mozilla.org/zh-CN/docs/Web/SVG
    //MathML,爲 http://www.w3.org/1998/Math/MathML,請參考:https://developer.mozilla.org/zh-CN/docs/Web/MathML
    //有興趣的,請參考:https://blog.csdn.net/qq_26440903/article/details/52592501
    parentNamespace = ((hostContext: any): HostContextProd);
  }
  //建立 DOM 元素
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  //建立指向 fiber 對象的屬性,方便從DOM 實例上獲取 fiber 對象
  precacheFiberNode(internalInstanceHandle, domElement);
  //建立指向 props 的屬性,方便從 DOM 實例上獲取 props
  updateFiberProps(domElement, props);
  return domElement;
}
複製代碼

解析:
(1) 一開始先肯定了命名空間,通常是htmlnamespacegit

SVGnamespacehttp://www.w3.org/2000/svg
請參考:
developer.mozilla.org/zh-CN/docs/…github

MathMLnamespacehttp://www.w3.org/1998/Math/MathML
請參考:
developer.mozilla.org/zh-CN/docs/…web

(2) 執行createElement(),建立DOM對象

(3) 執行precacheFiberNode(),在DOM對象上建立指向fiber對象的屬性:'__reactInternalInstance$'+Math.random().toString(36).slice(2),方便從DOM對象上獲取fiber對象

(4) 執行updateFiberProps(),在DOM對象上建立指向props的屬性:__reactEventHandlers$'+Math.random().toString(36).slice(2),方便從DOM實例上獲取props

(5) 最後,返回該DOM元素:

咱們來看下createElement()precacheFiberNode()updateFiberProps()

3、createElement
做用:
建立DOM元素

源碼:

export function createElement(
  type: string,
  props: Object,
  rootContainerElement: Element | Document,
  parentNamespace: string,
): Element 
{
  let isCustomComponentTag;

  // We create tags in the namespace of their parent container, except HTML
  // tags get no namespace.
  //獲取 document 對象
  const ownerDocument: Document = getOwnerDocumentFromRootContainer(
    rootContainerElement,
  );
  let domElement: Element;
  let namespaceURI = parentNamespace;
  if (namespaceURI === HTML_NAMESPACE) {
    //根據 DOM 實例的標籤獲取相應的命名空間
    namespaceURI = getIntrinsicNamespace(type);
  }
  //若是是 html namespace 的話
  if (namespaceURI === HTML_NAMESPACE) {
    //刪除了 dev 代碼

    if (type === 'script') {
      // Create the script via .innerHTML so its "parser-inserted" flag is
      // set to true and it does not execute

      //parser-inserted 設置爲 true 表示瀏覽器已經處理了該`<script>`標籤
      //那麼該標籤就不會被當作腳本執行
      //https://segmentfault.com/a/1190000008299659
      const div = ownerDocument.createElement('div');
      div.innerHTML = '<script><' + '/script>'// eslint-disable-line
      // This is guaranteed to yield a script element.
      //HTMLScriptElement:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLScriptElement
      const firstChild = ((div.firstChild: any): HTMLScriptElement);
      domElement = div.removeChild(firstChild);
    }
    //若是須要更新的 props裏有 is 屬性的話,那麼建立該元素時,則爲它添加「is」attribute
    //參考:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is
    else if (typeof props.is === 'string') {
      // $FlowIssue `createElement` should be updated for Web Components
      domElement = ownerDocument.createElement(type, {is: props.is});
    }
    //建立 DOM 元素
    else {
      // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
      // See discussion in https://github.com/facebook/react/pull/6896
      // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240

      //由於 Firefox 的一個 bug,因此須要特殊處理「is」屬性

      domElement = ownerDocument.createElement(type);
      // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
      // attributes on `select`s needs to be added before `option`s are inserted.
      // This prevents:
      // - a bug where the `select` does not scroll to the correct option because singular
      //  `select` elements automatically pick the first item #13222
      // - a bug where the `select` set the first item as selected despite the `size` attribute #14239
      // See https://github.com/facebook/react/issues/13222
      // and https://github.com/facebook/react/issues/14239

      //<select>標籤須要在<option>子節點被插入以前,設置`multiple`和`size`屬性
      if (type === 'select') {
        const node = ((domElement: any): HTMLSelectElement);
        if (props.multiple) {
          node.multiple = true;
        } else if (props.size) {
          // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
          // it is possible that no option is selected.
          //
          // This is only necessary when a select in "single selection mode".
          node.size = props.size;
        }
      }
    }
  }
  //svg/math 的元素建立是須要指定命名空間 URI 的
  else {
    //建立一個具備指定的命名空間URI和限定名稱的元素
    //https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElementNS
    domElement = ownerDocument.createElementNS(namespaceURI, type);
  }

  //刪除了 dev 代碼

  return domElement;
}
複製代碼

(1) 執行getOwnerDocumentFromRootContainer(),獲取獲取根節點的document對象,
關於getOwnerDocumentFromRootContainer()源碼,請參考:
React源碼解析之completeWork和HostText的更新

(2) 執行getIntrinsicNamespace(),根據fiber對象的type,即標籤類型,獲取對應的命名空間:
getIntrinsicNamespace()

const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';

// Assumes there is no parent namespace.
//假設沒有父命名空間
//根據 DOM 實例的標籤獲取相應的命名空間
export function getIntrinsicNamespace(type: string): string {
  switch (type) {
    case 'svg':
      return SVG_NAMESPACE;
    case 'math':
      return MATH_NAMESPACE;
    default:
      return HTML_NAMESPACE;
  }
}
複製代碼

(3) 以後則是一個if...else的判斷,若是是html的命名空間的話,則須要對一些標籤進行特殊處理;
若是是SVG/MathML的話,則執行createElementNS(),建立一個具備指定的命名空間URI和限定名稱的元素,
請參考:
developer.mozilla.org/zh-CN/docs/…

(4) 絕大部分是走的if裏狀況,看一下處理了哪些標籤:

① 若是是<script>標籤的話,則經過div.innerHTML的形式插入該標籤,以禁止被瀏覽器當成腳本去執行

關於HTMLScriptElement,請參考:
developer.mozilla.org/zh-CN/docs/…

② 若是須要更新的props裏有is屬性的話,那麼建立該元素時,則爲它添加「is」attribute,
也就是自定義元素,
請參考:
developer.mozilla.org/zh-CN/docs/…

③ 除了上面兩種狀況外,則使用Document.createElement()建立元素

還有對<select>標籤的bug修復,瞭解下就好

4、precacheFiberNode
做用:
DOM對象上建立指向fiber對象的屬性

源碼:

const randomKey = Math.random()
  //轉成 36 進制
  .toString(36)
  //從index=2開始截取
  .slice(2);

const internalInstanceKey = '__reactInternalInstance$' + randomKey;

export function precacheFiberNode(hostInst, node{
  node[internalInstanceKey] = hostInst;
}
複製代碼

解析:
比較簡單,能夠學習下 React 取隨機數的技巧:

Math.random().toString(36).slice(2)
複製代碼

5、updateFiberProps
做用:
DOM對象上建立指向props的屬性

源碼:

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

export function updateFiberProps(node, props{
  node[internalEventHandlersKey] = props;
}
複製代碼

解析:
同上

是對createInstance()及其內部function的講解,接下來看下appendAllChildren()及其內部function

6、appendAllChildren
做用:
插入子節點

源碼:

appendAllChildren = function(
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean,
  ) {
    // We only have the top Fiber that was created but we need recurse down its
    // children to find all the terminal nodes.
    //獲取該節點的第一個子節點
    let node = workInProgress.child;
    //當該節點有子節點時
    while (node !== null) {
      //若是是原生節點或 text 節點的話
      if (node.tag === HostComponent || node.tag === HostText) {
        //將node.stateNode掛載到 parent 上
        //appendChild API:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
        appendInitialChild(parent, node.stateNode);
      } else if (node.tag === HostPortal) {
        // If we have a portal child, then we don't want to traverse
        // down its children. Instead, we'll get insertions from each child in
        // the portal directly.
      }
      //若是子節點還有子子節點的話
      else if (node.child !== null) {
        //return 指向復建點
        node.child.return = node;
        //一直循環,設置return 屬性,直到沒有子節點
        node = node.child;
        continue;
      }
      if (node === workInProgress) {
        return;
      }
      //若是沒有兄弟節點的話,返回至父節點
      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;
        }
        node = node.return;
      }
      //設置兄弟節點的 return 爲父節點
      node.sibling.return = node.return;
      //遍歷兄弟節點
      node = node.sibling;
    }
  };
複製代碼

解析:
(1) 基本邏輯是獲取目標節點下的第一個子節點,將其與父節點(即return屬性)關聯,子子節點也是如此,循環往復;

而後依次遍歷兄弟節點,將其與父節點(即return屬性)關聯,最終會造成以下圖的關係:

(2) appendInitialChild()

export function appendInitialChild(
  parentInstance: Instance,
  child: Instance | TextInstance,
): void 
{
  parentInstance.appendChild(child);
}
複製代碼

本質就是調用appendChild()這個 API

是對appendAllChildren()及其內部function的講解,接下來看下finalizeInitialChildren()及其內部function,接下來內容會不少

7、finalizeInitialChildren
做用:
(1) 初始化DOM對象的事件監聽器和內部屬性
(2) 返回autoFocus屬性的布爾值

源碼:

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean 
{
  //初始化 DOM 對象
  //一、對一些標籤進行事件綁定/屬性的特殊處理
  //二、對 DOM 對象內部屬性進行初始化
  setInitialProperties(domElement, type, props, rootContainerInstance);
  //能夠 foucus 的節點返回autoFocus的值,不然返回 false
  return shouldAutoFocusHostComponent(type, props);
}
複製代碼

解析:
(1) 執行setInitialProperties(),對一些標籤進行事件綁定/屬性的特殊處理,而且對DOM對象內部屬性進行初始化

(2) 執行shouldAutoFocusHostComponent(),能夠foucus的節點會返回autoFocus的值,不然返回false

8、setInitialProperties
做用:
初始化DOM對象

源碼:

export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document,
): void 
{
  //判斷是不是自定義的 DOM 標籤
  const isCustomComponentTag = isCustomComponent(tag, rawProps);
  //刪除了 dev 代碼

  // TODO: Make sure that we check isMounted before firing any of these events.
  //確保在觸發這些監聽器觸發之間,已經初始化了 event
  let props: Object;
  switch (tag) {
    case 'iframe':
    case 'object':
    case 'embed':
      //load listener
      //React 自定義的綁定事件,暫時跳過
      trapBubbledEvent(TOP_LOAD, domElement);
      props = rawProps;
      break;
    case 'video':
    case 'audio':
      // Create listener for each media event
      //初始化 media 標籤的監聽器

      // export const mediaEventTypes = [
      //   TOP_ABORT, //abort
      //   TOP_CAN_PLAY, //canplay
      //   TOP_CAN_PLAY_THROUGH, //canplaythrough
      //   TOP_DURATION_CHANGE, //durationchange
      //   TOP_EMPTIED, //emptied
      //   TOP_ENCRYPTED, //encrypted
      //   TOP_ENDED, //ended
      //   TOP_ERROR, //error
      //   TOP_LOADED_DATA, //loadeddata
      //   TOP_LOADED_METADATA, //loadedmetadata
      //   TOP_LOAD_START, //loadstart
      //   TOP_PAUSE, //pause
      //   TOP_PLAY, //play
      //   TOP_PLAYING, //playing
      //   TOP_PROGRESS, //progress
      //   TOP_RATE_CHANGE, //ratechange
      //   TOP_SEEKED, //seeked
      //   TOP_SEEKING, //seeking
      //   TOP_STALLED, //stalled
      //   TOP_SUSPEND, //suspend
      //   TOP_TIME_UPDATE, //timeupdate
      //   TOP_VOLUME_CHANGE, //volumechange
      //   TOP_WAITING, //waiting
      // ];

      for (let i = 0; i < mediaEventTypes.length; i++) {
        trapBubbledEvent(mediaEventTypes[i], domElement);
      }
      props = rawProps;
      break;
    case 'source':
      //error listener
      trapBubbledEvent(TOP_ERROR, domElement);
      props = rawProps;
      break;
    case 'img':
    case 'image':
    case 'link':
      //error listener
      trapBubbledEvent(TOP_ERROR, domElement);
      //load listener
      trapBubbledEvent(TOP_LOAD, domElement);
      props = rawProps;
      break;
    case 'form':
      //reset listener
      trapBubbledEvent(TOP_RESET, domElement);
      //submit listener
      trapBubbledEvent(TOP_SUBMIT, domElement);
      props = rawProps;
      break;
    case 'details':
      //toggle listener
      trapBubbledEvent(TOP_TOGGLE, domElement);
      props = rawProps;
      break;
    case 'input':
      //在 input 對應的 DOM 節點上新建_wrapperState屬性
      ReactDOMInputInitWrapperState(domElement, rawProps);
      //淺拷貝value/checked等屬性
      props = ReactDOMInputGetHostProps(domElement, rawProps);
      //invalid listener
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      //初始化 onChange listener
      //https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral
      //暫時跳過
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    case 'option':
      //dev 環境下
      //一、判斷<option>標籤的子節點是不是 number/string
      //二、判斷是否正確設置defaultValue/value
      ReactDOMOptionValidateProps(domElement, rawProps);
      //獲取 option 的 child
      props = ReactDOMOptionGetHostProps(domElement, rawProps);
      break;
    case 'select':
      //在 select 對應的 DOM 節點上新建_wrapperState屬性
      ReactDOMSelectInitWrapperState(domElement, rawProps);
      //設置<select>對象屬性
      props = ReactDOMSelectGetHostProps(domElement, rawProps);
      //invalid listener
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      //初始化 onChange listener
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    case 'textarea':
      //在 textarea 對應的 DOM 節點上新建_wrapperState屬性
      ReactDOMTextareaInitWrapperState(domElement, rawProps);
      //設置 textarea 內部屬性
      props = ReactDOMTextareaGetHostProps(domElement, rawProps);
      //invalid listener
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      //初始化 onChange listener
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    default:
      props = rawProps;
  }
  //判斷新屬性,好比 style 是否正確賦值
  assertValidProps(tag, props);
  //設置初始的 DOM 對象屬性
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );
  //對特殊的 DOM 標籤進行最後的處理
  switch (tag) {
    case 'input':
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      //
      track((domElement: any));
      ReactDOMInputPostMountWrapper(domElement, rawProps, false);
      break;
    case 'textarea':
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      track((domElement: any));
      ReactDOMTextareaPostMountWrapper(domElement, rawProps);
      break;
    case 'option':
      ReactDOMOptionPostMountWrapper(domElement, rawProps);
      break;
    case 'select':
      ReactDOMSelectPostMountWrapper(domElement, rawProps);
      break;
    default:
      if (typeof props.onClick === 'function') {
        // TODO: This cast may not be sound for SVG, MathML or custom elements.
        //初始化 onclick 事件,以便兼容Safari移動端
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      break;
  }
}
複製代碼

解析:
(1) 判斷是否 是自定義的DOM標籤,執行isCustomComponent(),返回true/false

isCustomComponent()

function isCustomComponent(tagName: string, props: Object{
  //通常自定義標籤的命名規則是帶`-`的
  if (tagName.indexOf('-') === -1) {
    //https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is
    return typeof props.is === 'string';
  }
  //如下的是SVG/MathML的標籤屬性
  switch (tagName) {
    // These are reserved SVG and MathML elements.
    // We don't mind this whitelist too much because we expect it to never grow.
    // The alternative is to track the namespace in a few places which is convoluted.
    // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
    case 'annotation-xml':
    case 'color-profile':
    case 'font-face':
    case 'font-face-src':
    case 'font-face-uri':
    case 'font-face-format':
    case 'font-face-name':
    case 'missing-glyph':
      return false;
    default:
      return true;
  }
}
複製代碼

(2) 而後是對一些標籤,進行一些額外的處理,如初始化特殊的事件監聽、初始化特殊的屬性(通常的標籤是沒有的)等

(3) 看下對<input>標籤的處理:
① 執行ReactDOMInputInitWrapperState(),在<input>對應的DOM節點上新建_wrapperState屬性

ReactDOMInputInitWrapperState()

//在 input 對應的 DOM 節點上新建_wrapperState屬性
export function initWrapperState(element: Element, props: Object{
  //刪除了 dev 代碼

  const node = ((element: any): InputWithWrapperState);
  //Input 的默認值
  const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
  //在 input 對應的 DOM 節點上新建_wrapperState屬性
  node._wrapperState = {
    //input 有 radio/checkbox 類型,checked 即判斷單/多選框是否被選中
    initialChecked:
      props.checked != null ? props.checked : props.defaultChecked,
    //input 的初始值,優先選擇 value,其次 defaultValue
    initialValue: getToStringValue(
      props.value != null ? props.value : defaultValue,
    ),
    //radio/checkbox
    //若是type 爲 radio/checkbox 的話,看 checked 有沒有被選中
    //若是是其餘 type 的話,則看 value 是否有值
    controlled: isControlled(props),
  };
}

export function getToStringValue(value: mixed): ToStringValue {
  switch (typeof value) {
    case 'boolean':
    case 'number':
    case 'object':
    case 'string':
    case 'undefined':
      return value;
    default:
      // function, symbol are assigned as empty strings
      return '';
  }
}

function isControlled(props{
  const usesChecked = props.type === 'checkbox' || props.type === 'radio';
  return usesChecked ? props.checked != null : props.value != null;
}
複製代碼

② 執行ReactDOMInputGetHostProps(),淺拷貝、初始化value/checked等屬性

getHostProps()

//淺拷貝value/checked等屬性
export function getHostProps(element: Element, props: Object{
  const node = ((element: any): InputWithWrapperState);
  const checked = props.checked;
  //淺拷貝
  const hostProps = Object.assign({}, props, {
    defaultCheckedundefined,
    defaultValueundefined,
    valueundefined,
    checked: checked != null ? checked : node._wrapperState.initialChecked,
  });

  return hostProps;
}
複製代碼

③ 執行ensureListeningTo(),初始化onChange listener

(4) 看下對<option>標籤的處理:

① 執行ReactDOMOptionValidateProps(),在 dev 環境下:
[1] 判斷

ReactDOMOptionValidateProps()

export function validateProps(element: Element, props: Object{
  if (__DEV__) {
    // This mirrors the codepath above, but runs for hydration too.
    // Warn about invalid children here so that client and hydration are consistent.
    // TODO: this seems like it could cause a DEV-only throw for hydration
    // if children contains a non-element object. We should try to avoid that.
    if (typeof props.children === 'object' && props.children !== null) {
      React.Children.forEach(props.children, function(child{
        if (child == null) {
          return;
        }
        if (typeof child === 'string' || typeof child === 'number') {
          return;
        }
        if (typeof child.type !== 'string') {
          return;
        }
        if (!didWarnInvalidChild) {
          didWarnInvalidChild = true;
          warning(
            false,
            'Only strings and numbers are supported as <option> children.',
          );
        }
      });
    }

    // TODO: Remove support for `selected` in <option>.
    if (props.selected != null && !didWarnSelectedSetOnOption) {
      warning(
        false,
        'Use the `defaultValue` or `value` props on <select> instead of ' +
          'setting `selected` on <option>.',
      );
      didWarnSelectedSetOnOption = true;
    }
  }
}
複製代碼

② 執行ReactDOMOptionGetHostProps(),獲取optionchild

ReactDOMOptionGetHostProps()

//獲取<option>child 的內容,而且展平 children
export function getHostProps(element: Element, props: Object{
  const hostProps = {childrenundefined, ...props};
  //展平 child,可參考我以前寫的一篇:https://juejin.im/post/5d46b71a6fb9a06b0c084acd
  const content = flattenChildren(props.children);

  if (content) {
    hostProps.children = content;
  }

  return hostProps;
}
複製代碼

可參考:
React源碼解析之React.children.map()

(5) 看下對< select>標籤的處理:
① 執行ReactDOMSelectInitWrapperState(),在select對應的DOM節點上新建_wrapperState屬性

ReactDOMSelectInitWrapperState()

export function initWrapperState(element: Element, props: Object{
  const node = ((element: any): SelectWithWrapperState);
  //刪除了 dev 代碼

  node._wrapperState = {
    wasMultiple: !!props.multiple,
  };

  //刪除了 dev 代碼
}
複製代碼

② 執行ReactDOMSelectGetHostProps(),設置<select>對象屬性

ReactDOMSelectGetHostProps()

//設置<select>對象屬性
//{
// children:[],
// value:undefined
// }
export function getHostProps(element: Element, props: Object{
  return Object.assign({}, props, {
    valueundefined,
  });
}
複製代碼

③ 執行trapBubbledEvent(),初始化invalid listener

④ 執行ensureListeningTo(),初始化onChange listener

(6) <textarea>標籤的處理邏輯,同上,簡單看下它的源碼:

ReactDOMTextareaInitWrapperState()

//在 textarea 對應的 DOM 節點上新建_wrapperState屬性
export function initWrapperState(element: Element, props: Object{
  const node = ((element: any): TextAreaWithWrapperState);
  //刪除了 dev 代碼

  //textArea 裏面的值
  let initialValue = props.value;

  // Only bother fetching default value if we're going to use it
  if (initialValue == null) {
    let defaultValue = props.defaultValue;
    // TODO (yungsters): Remove support for children content in <textarea>.
    let children = props.children;
    if (children != null) {
      //刪除了 dev 代碼

      invariant(
        defaultValue == null,
        'If you supply `defaultValue` on a <textarea>, do not pass children.',
      );
      if (Array.isArray(children)) {
        invariant(
          children.length <= 1,
          '<textarea> can only have at most one child.',
        );
        children = children[0];
      }

      defaultValue = children;
    }
    if (defaultValue == null) {
      defaultValue = '';
    }
    initialValue = defaultValue;
  }

  node._wrapperState = {
    initialValue: getToStringValue(initialValue),
  };
}
複製代碼

ReactDOMTextareaGetHostProps()

//設置 textarea 內部屬性
export function getHostProps(element: Element, props: Object{
  const node = ((element: any): TextAreaWithWrapperState);
  //若是設置 innerHTML 的話,提醒開發者無效
  invariant(
    props.dangerouslySetInnerHTML == null,
    '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
  );

  // Always set children to the same thing. In IE9, the selection range will
  // get reset if `textContent` is mutated.  We could add a check in setTextContent
  // to only set the value if/when the value differs from the node value (which would
  // completely solve this IE9 bug), but Sebastian+Sophie seemed to like this
  // solution. The value can be a boolean or object so that's why it's forced
  // to be a string.

  //設置 textarea 內部屬性
  const hostProps = {
    ...props,
    valueundefined,
    defaultValueundefined,
    children: toString(node._wrapperState.initialValue),
  };

  return hostProps;
}
複製代碼

(7) 標籤內部屬性和事件監聽器特殊處理完後,就執行assertValidProps(),判斷新屬性,好比 style是否正確賦值

assertValidProps()

//判斷新屬性,好比 style 是否正確賦值
function assertValidProps(tag: string, props: ?Object{
  if (!props) {
    return;
  }
  // Note the use of `==` which checks for null or undefined.
  //判斷目標節點的標籤是否能夠包含子標籤,如 <br/>、<input/> 等是不能包含子標籤的
  if (voidElementTags[tag]) {
    //不能包含子標籤,報出 error
    invariant(
      props.children == null && props.dangerouslySetInnerHTML == null,
      '%s is a void element tag and must neither have `children` nor ' +
        'use `dangerouslySetInnerHTML`.%s',
      tag,
      __DEV__ ? ReactDebugCurrentFrame.getStackAddendum() : '',
    );
  }
  //__html設置的標籤內有子節點,好比:__html:"<span>aaa</span>" ,就會報錯
  if (props.dangerouslySetInnerHTML != null) {
    invariant(
      props.children == null,
      'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
    );
    invariant(
      typeof props.dangerouslySetInnerHTML === 'object' &&
        HTML in props.dangerouslySetInnerHTML,
      '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
        'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' +
        'for more information.',
    );
  }
  //刪除了 dev 代碼

  //style 不爲 null,可是不是 Object 類型的話,報如下錯誤
  invariant(
    props.style == null || typeof props.style === 'object',
    'The `style` prop expects a mapping from style properties to values, ' +
      "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
      'using JSX.%s',
    __DEV__ ? ReactDebugCurrentFrame.getStackAddendum() : '',
  );
}
複製代碼

(8) 執行setInitialDOMProperties(),設置初始的 DOM 對象屬性,比較長

setInitialDOMProperties()

//初始化 DOM 對象的內部屬性
function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void 
{
  //循環新 props
  for (const propKey in nextProps) {
    //原型鏈上的屬性不做處理
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    //獲取 prop 的值
    const nextProp = nextProps[propKey];
    //設置 style 屬性
    if (propKey === STYLE) {
      //刪除了 dev 代碼

      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      //設置 style 的值
      setValueForStyles(domElement, nextProp);
    }
    //設置 innerHTML 屬性
    else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    }
    //設置子節點
    else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        // Avoid setting initial textContent when the text is empty. In IE11 setting
        // textContent on a <textarea> will cause the placeholder to not
        // show within the <textarea> until it has been focused and blurred again.
        // https://github.com/facebook/react/issues/6731#issuecomment-254874553

        //當 text 沒有時,禁止設置初始內容
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);
        }
      }
      //number 的話轉成 string
      else if (typeof nextProp === 'number') {

        setTextContent(domElement, '' + nextProp);
      }
    } else if (
      propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
      propKey === SUPPRESS_HYDRATION_WARNING
    ) {
      // Noop
    } else if (propKey === AUTOFOCUS) {
      // We polyfill it separately on the client during commit.
      // We could have excluded it in the property list instead of
      // adding a special case here, but then it wouldn't be emitted
      // on server rendering (but we *do* want to emit it in SSR).
    }
    //若是有綁定事件的話,如<div onClick=(()=>{ xxx })></div>
    else if (registrationNameModules.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        //刪除了 dev 代碼
        //https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral
        ensureListeningTo(rootContainerElement, propKey);
      }
    } else if (nextProp != null) {
      //爲 DOM 節點設置屬性值
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}
複製代碼

邏輯是循環DOM對象上的新props,對不一樣的狀況作相應的處理

① 若是是style的話,則執行setValueForStyles(),確保 正確初始化style屬性:

setValueForStyles()

// 設置 style 的值
export function setValueForStyles(node, styles{
  const style = node.style;
  for (let styleName in styles) {
    if (!styles.hasOwnProperty(styleName)) {
      continue;
    }
    //沒有找到關於自定義樣式名的資料。。
    //可參考:https://zh-hans.reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html
    const isCustomProperty = styleName.indexOf('--') === 0;
    //刪除了 dev 代碼
    //確保樣式的 value 是正確的
    const styleValue = dangerousStyleValue(
      styleName,
      styles[styleName],
      isCustomProperty,
    );
    //將 float 屬性重命名
    //<div style={{float:'left',}}></div>
    if (styleName === 'float') {
      styleName = 'cssFloat';
    }
    if (isCustomProperty) {
      style.setProperty(styleName, styleValue);
    } else {
      //正確設置 style 對象內的值
      style[styleName] = styleValue;
    }
  }
}
複製代碼

dangerousStyleValue(),確保樣式的value是正確的:

//確保樣式的 value 是正確的
function dangerousStyleValue(name, value, isCustomProperty{
  // Note that we've removed escapeTextForBrowser() calls here since the
  // whole string will be escaped when the attribute is injected into
  // the markup. If you provide unsafe user data here they can inject
  // arbitrary CSS which may be problematic (I couldn't repro this):
  // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
  // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
  // This is not an XSS hole but instead a potential CSS injection issue
  // which has lead to a greater discussion about how we're going to
  // trust URLs moving forward. See #2115901

  const isEmpty = value == null || typeof value === 'boolean' || value === '';
  if (isEmpty) {
    return '';
  }

  if (
    //-webkit-transform/-moz-transform/-ms-transform
    !isCustomProperty &&
    typeof value === 'number' &&
    value !== 0 &&
    !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name])
  ) {
    //將 React上的 style 裏的對象的值轉成 px
    return value + 'px'// Presumes implicit 'px' suffix for unitless numbers
  }

  return ('' + value).trim();
}
複製代碼

② 若是是innerHTML的話,則執行setInnerHTML(),設置innerHTML屬性

setInnerHTML()

const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
  node: Element,
  html: string,
): void 
{
  // IE does not have innerHTML for SVG nodes, so instead we inject the
  // new markup in a temp node and then move the child nodes across into
  // the target node

  //兼容 IE
  if (node.namespaceURI === Namespaces.svg && !('innerHTML' in node)) {
    reusableSVGContainer =
      reusableSVGContainer || document.createElement('div');
    reusableSVGContainer.innerHTML = '<svg>' + html + '</svg>';
    const svgNode = reusableSVGContainer.firstChild;
    while (node.firstChild) {
      node.removeChild(node.firstChild);
    }
    while (svgNode.firstChild) {
      node.appendChild(svgNode.firstChild);
    }
  } else {
    node.innerHTML = html;
  }
});
複製代碼

③ 若是是children的話,當子節點是string/number時,執行setTextContent(),設置textContent屬性

setTextContent()

let setTextContent = function(node: Element, text: string): void {
  if (text) {
    let firstChild = node.firstChild;

    if (
      firstChild &&
      firstChild === node.lastChild &&
      firstChild.nodeType === TEXT_NODE
    ) {
      firstChild.nodeValue = text;
      return;
    }
  }
  node.textContent = text;
};
複製代碼

④ 若是有綁定事件的話,如<div onClick=(()=>{ xxx })></div>,則執行,確保綁定到了document上,請參考:

www.cnblogs.com/Darlietooth…

registrationNameModules

⑤ 不是上述狀況的話,則setValueForProperty(),爲DOM節點設置屬性值(這個 function 太長了,暫時跳過)

(9) 最後又是一串switch...case,對特殊的DOM標籤進行最後的處理,瞭解下就好

9、shouldAutoFocusHostComponent
做用:
能夠foucus的節點會返回autoFocus的值,不然返回false

源碼:

//能夠 foucus 的節點返回autoFocus的值,不然返回 false
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
  //能夠 foucus 的節點返回autoFocus的值,不然返回 false
  switch (type) {
    case 'button':
    case 'input':
    case 'select':
    case 'textarea':
      return !!props.autoFocus;
  }
  return false;
}
複製代碼

解析:
比較簡單

是對finalizeInitialChildren()及其內部function的解析,本文也到此結束了,最後放上 GitHub

10、GitHub
ReactFiberCompleteWork.js
github.com/AttackXiaoJ…

ReactDOMHostConfig.js
github.com/AttackXiaoJ…

ReactDOMComponent.js
github.com/AttackXiaoJ…


(完)

相關文章
相關標籤/搜索