天天看點

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

前言

在上篇 React源碼解析之HostComponent的更新(上) 中,我們講到了多次渲染階段的更新,本篇我們講第一次渲染階段的更新

一、HostComponent(第一次渲染)

作用:

(1) 建立 DOM 執行個體

(2) 插入子節點

(3) 初始化事件監聽器

源碼:

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 執行個體
           //1、建立 DOM 元素
           //2、建立指向 fiber 對象的屬性,友善從DOM 執行個體上擷取 fiber 對象
           //3、建立指向 props 的屬性,友善從 DOM 執行個體上擷取 props
          let instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          //插入子節點
          appendAllChildren(instance, workInProgress, false, false);

          // 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

即可

我們先來看下

createInstance()

方法

二、createInstance

作用:

建立

DOM

對象

源碼:

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) 一開始先确定了命名空間,一般是

html

namespace

SVG

namespace

http://www.w3.org/2000/svg

請參考:

https://developer.mozilla.org/zh-CN/docs/Web/SVG

MathML

namespace

http://www.w3.org/1998/Math/MathML

請參考:

https://developer.mozilla.org/zh-CN/docs/Web/MathML

(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

元素:

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

我們來看下

createElement()

precacheFiberNode()

updateFiberProps()

三、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和限定名稱的元素, 請參考:

https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElementNS

(4) 絕大部分是走的

if

裡情況,看一下處理了哪些标簽:

① 如果是

<script>

标簽的話,則通過

div.innerHTML

的形式插入該标簽,以禁止被浏覽器當成腳本去執行

關于

HTMLScriptElement

,請參考:

https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLScriptElement

② 如果需要更新的

props

裡有

is

屬性的話,那麼建立該元素時,則為它添加「is」attribute, 也就是自定義元素,

請參考:

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is

③ 除了上面兩種情況外,則使用

Document.createElement()

建立元素

還有對

<select>

标簽的

bug

修複,了解下就好

四、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)
           

複制

五、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

六、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

屬性)關聯,最終會形成如下圖的關系:

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

(2)

appendInitialChild()

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

複制

本質就是調用

appendChild()

這個 API

是對

appendAllChildren()

及其内部

function

的講解,接下來看下

finalizeInitialChildren()

及其内部

function

,接下來内容會很多

七、finalizeInitialChildren

作用:

(1) 初始化

DOM

對象的事件監聽器和内部屬性

(2) 傳回

autoFocus

屬性的布爾值

源碼:

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  //初始化 DOM 對象
  //1、對一些标簽進行事件綁定/屬性的特殊處理
  //2、對 DOM 對象内部屬性進行初始化
  setInitialProperties(domElement, type, props, rootContainerInstance);
  //可以 foucus 的節點傳回autoFocus的值,否則傳回 false
  return shouldAutoFocusHostComponent(type, props);
}
           

複制

解析:

(1) 執行

setInitialProperties()

,對一些标簽進行事件綁定/屬性的特殊處理,并且對

DOM

對象内部屬性進行初始化

(2) 執行

shouldAutoFocusHostComponent()

,可以

foucus

的節點會傳回

autoFocus

的值,否則傳回

false

八、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 環境下
      //1、判斷<option>标簽的子節點是否是 number/string
      //2、判斷是否正确設定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, {
    defaultChecked: undefined,
    defaultValue: undefined,
    value: undefined,
    checked: checked != null ? checked : node._wrapperState.initialChecked,
  });

  return hostProps;
}
           

複制

③ 執行

ensureListeningTo()

,初始化

onChange listener

(4) 看下對

< option>

标簽的處理:

① 執行

ReactDOMOptionValidateProps()

,在 dev 環境下:

[1] 判斷

<option>

标簽的子節點是否是

number/string

[2] 判斷是否正确設定

defaultValue/value

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()

,擷取

option

child

ReactDOMOptionGetHostProps()

//擷取<option>child 的内容,并且展平 children
export function getHostProps(element: Element, props: Object) {
  const hostProps = {children: undefined, ...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, {
    value: undefined,
  });
}
           

複制

③ 執行

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,
    value: undefined,
    defaultValue: undefined,
    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

上,請參考:

https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral

registrationNameModules

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

⑤ 不是上述情況的話,則

setValueForProperty()

,為

DOM

節點設定屬性值(這個 function 太長了,暫時跳過)

(9) 最後又是一串

switch...case

,對特殊的

DOM

标簽進行最後的處理,了解下就好

九、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

GitHub

ReactFiberCompleteWork.js

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCompleteWork.js

ReactDOMHostConfig.js

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/ReactDOMHostConfig.js

ReactDOMComponent.js

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/Reac