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

  • 2020 年 3 月 18 日
  • 筆記

前言

在上篇 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) 一開始先確定了命名空間,一般是htmlnamespace

SVGnamespacehttp://www.w3.org/2000/svg, 請參考: https://developer.mozilla.org/zh-CN/docs/Web/SVG

MathMLnamespacehttp://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元素:

我們來看下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屬性)關聯,最終會形成如下圖的關係:

(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(),獲取optionchild

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

⑤ 不是上述情況的話,則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