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

我們來看下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()
,獲取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
:

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