前言:
在上篇 React源碼解析之HostComponent的更新(上) 中,咱們講到了屢次渲染階段的更新,本篇咱們講第一次渲染階段的更新javascript
1、HostComponent(第一次渲染)
做用:
(1) 建立 DOM 實例
(2) 插入子節點
(3) 初始化事件監聽器php
源碼:css
else {
//若是是第一次渲染的話
//若是沒有新 props 更新,可是執行到這裏的話,多是 React 內部出現了問題
if (!newProps) {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
break;
}
//context 相關,暫時跳過
const currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on we want to add then top->down or
// bottom->up. Top->down is faster in IE11.
//是否曾是服務端渲染
let wasHydrated = popHydrationState(workInProgress);
//若是是服務端渲染的話,暫時跳過
if (wasHydrated) {
//暫時刪除
}
//不是服務端渲染
else {
//建立 DOM 實例
//一、建立 DOM 元素
//二、建立指向 fiber 對象的屬性,方便從DOM 實例上獲取 fiber 對象
//三、建立指向 props 的屬性,方便從 DOM 實例上獲取 props
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
//插入子節點
appendAllChildren(instance, workInProgress, 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
便可html
咱們先來看下createInstance()
方法java
2、createInstance
做用:
建立DOM
對象node
源碼:react
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
if (__DEV__) {
//刪除了 dev 代碼
} else {
//肯定該節點的命名空間
// 通常是HTML,http://www.w3.org/1999/xhtml
//svg,爲 http://www.w3.org/2000/svg ,請參考:https://developer.mozilla.org/zh-CN/docs/Web/SVG
//MathML,爲 http://www.w3.org/1998/Math/MathML,請參考:https://developer.mozilla.org/zh-CN/docs/Web/MathML
//有興趣的,請參考:https://blog.csdn.net/qq_26440903/article/details/52592501
parentNamespace = ((hostContext: any): HostContextProd);
}
//建立 DOM 元素
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
//建立指向 fiber 對象的屬性,方便從DOM 實例上獲取 fiber 對象
precacheFiberNode(internalInstanceHandle, domElement);
//建立指向 props 的屬性,方便從 DOM 實例上獲取 props
updateFiberProps(domElement, props);
return domElement;
}
複製代碼
解析:
(1) 一開始先肯定了命名空間,通常是html
的namespace
git
SVG
的namespace
爲http://www.w3.org/2000/svg
,
請參考:
developer.mozilla.org/zh-CN/docs/…github
MathML
的namespace
爲http://www.w3.org/1998/Math/MathML
,
請參考:
developer.mozilla.org/zh-CN/docs/…web
(2) 執行createElement()
,建立DOM
對象
(3) 執行precacheFiberNode()
,在DOM
對象上建立指向fiber
對象的屬性:'__reactInternalInstance$'+Math.random().toString(36).slice(2)
,方便從DOM
對象上獲取fiber
對象
(4) 執行updateFiberProps()
,在DOM
對象上建立指向props
的屬性:__reactEventHandlers$'+Math.random().toString(36).slice(2)
,方便從DOM
實例上獲取props
(5) 最後,返回該DOM
元素:
咱們來看下createElement()
、precacheFiberNode()
和updateFiberProps()
3、createElement
做用:
建立DOM
元素
源碼:
export function createElement(
type: string,
props: Object,
rootContainerElement: Element | Document,
parentNamespace: string,
): Element {
let isCustomComponentTag;
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
//獲取 document 對象
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
rootContainerElement,
);
let domElement: Element;
let namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
//根據 DOM 實例的標籤獲取相應的命名空間
namespaceURI = getIntrinsicNamespace(type);
}
//若是是 html namespace 的話
if (namespaceURI === HTML_NAMESPACE) {
//刪除了 dev 代碼
if (type === 'script') {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
//parser-inserted 設置爲 true 表示瀏覽器已經處理了該`<script>`標籤
//那麼該標籤就不會被當作腳本執行
//https://segmentfault.com/a/1190000008299659
const div = ownerDocument.createElement('div');
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
// This is guaranteed to yield a script element.
//HTMLScriptElement:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLScriptElement
const firstChild = ((div.firstChild: any): HTMLScriptElement);
domElement = div.removeChild(firstChild);
}
//若是須要更新的 props裏有 is 屬性的話,那麼建立該元素時,則爲它添加「is」attribute
//參考:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is
else if (typeof props.is === 'string') {
// $FlowIssue `createElement` should be updated for Web Components
domElement = ownerDocument.createElement(type, {is: props.is});
}
//建立 DOM 元素
else {
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
//由於 Firefox 的一個 bug,因此須要特殊處理「is」屬性
domElement = ownerDocument.createElement(type);
// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
// attributes on `select`s needs to be added before `option`s are inserted.
// This prevents:
// - a bug where the `select` does not scroll to the correct option because singular
// `select` elements automatically pick the first item #13222
// - a bug where the `select` set the first item as selected despite the `size` attribute #14239
// See https://github.com/facebook/react/issues/13222
// and https://github.com/facebook/react/issues/14239
//<select>標籤須要在<option>子節點被插入以前,設置`multiple`和`size`屬性
if (type === 'select') {
const node = ((domElement: any): HTMLSelectElement);
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
// Setting a size greater than 1 causes a select to behave like `multiple=true`, where
// it is possible that no option is selected.
//
// This is only necessary when a select in "single selection mode".
node.size = props.size;
}
}
}
}
//svg/math 的元素建立是須要指定命名空間 URI 的
else {
//建立一個具備指定的命名空間URI和限定名稱的元素
//https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElementNS
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
//刪除了 dev 代碼
return domElement;
}
複製代碼
(1) 執行getOwnerDocumentFromRootContainer()
,獲取獲取根節點的document
對象,
關於getOwnerDocumentFromRootContainer()
源碼,請參考:
React源碼解析之completeWork和HostText的更新
(2) 執行getIntrinsicNamespace()
,根據fiber
對象的type
,即標籤類型,獲取對應的命名空間:getIntrinsicNamespace()
:
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
// Assumes there is no parent namespace.
//假設沒有父命名空間
//根據 DOM 實例的標籤獲取相應的命名空間
export function getIntrinsicNamespace(type: string): string {
switch (type) {
case 'svg':
return SVG_NAMESPACE;
case 'math':
return MATH_NAMESPACE;
default:
return HTML_NAMESPACE;
}
}
複製代碼
(3) 以後則是一個if...else
的判斷,若是是html
的命名空間的話,則須要對一些標籤進行特殊處理;
若是是SVG/MathML
的話,則執行createElementNS()
,建立一個具備指定的命名空間URI和限定名稱的元素,
請參考:
developer.mozilla.org/zh-CN/docs/…
(4) 絕大部分是走的if
裏狀況,看一下處理了哪些標籤:
① 若是是<script>
標籤的話,則經過div.innerHTML
的形式插入該標籤,以禁止被瀏覽器當成腳本去執行
關於HTMLScriptElement
,請參考:
developer.mozilla.org/zh-CN/docs/…
② 若是須要更新的props
裏有is
屬性的話,那麼建立該元素時,則爲它添加「is」attribute,
也就是自定義元素,
請參考:
developer.mozilla.org/zh-CN/docs/…
③ 除了上面兩種狀況外,則使用Document.createElement()
建立元素
還有對<select>
標籤的bug
修復,瞭解下就好
4、precacheFiberNode
做用:
在DOM
對象上建立指向fiber
對象的屬性
源碼:
const randomKey = Math.random()
//轉成 36 進制
.toString(36)
//從index=2開始截取
.slice(2);
const internalInstanceKey = '__reactInternalInstance$' + randomKey;
export function precacheFiberNode(hostInst, node) {
node[internalInstanceKey] = hostInst;
}
複製代碼
解析:
比較簡單,能夠學習下 React 取隨機數的技巧:
Math.random().toString(36).slice(2)
複製代碼
5、updateFiberProps
做用:
在DOM
對象上建立指向props
的屬性
源碼:
const randomKey = Math.random().toString(36).slice(2);
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
export function updateFiberProps(node, props) {
node[internalEventHandlersKey] = props;
}
複製代碼
解析:
同上
二
到五
是對createInstance()
及其內部function
的講解,接下來看下appendAllChildren()
及其內部function
6、appendAllChildren
做用:
插入子節點
源碼:
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
//獲取該節點的第一個子節點
let node = workInProgress.child;
//當該節點有子節點時
while (node !== null) {
//若是是原生節點或 text 節點的話
if (node.tag === HostComponent || node.tag === HostText) {
//將node.stateNode掛載到 parent 上
//appendChild API:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
}
//若是子節點還有子子節點的話
else if (node.child !== null) {
//return 指向復建點
node.child.return = node;
//一直循環,設置return 屬性,直到沒有子節點
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
//若是沒有兄弟節點的話,返回至父節點
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
//設置兄弟節點的 return 爲父節點
node.sibling.return = node.return;
//遍歷兄弟節點
node = node.sibling;
}
};
複製代碼
解析:
(1) 基本邏輯是獲取目標節點下的第一個子節點,將其與父節點(即return
屬性)關聯,子子節點也是如此,循環往復;
而後依次遍歷兄弟節點,將其與父節點(即return
屬性)關聯,最終會造成以下圖的關係:
(2) appendInitialChild()
:
export function appendInitialChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
parentInstance.appendChild(child);
}
複製代碼
本質就是調用appendChild()
這個 API
六
是對appendAllChildren()
及其內部function
的講解,接下來看下finalizeInitialChildren()
及其內部function
,接下來內容會不少
7、finalizeInitialChildren
做用:
(1) 初始化DOM
對象的事件監聽器和內部屬性
(2) 返回autoFocus
屬性的布爾值
源碼:
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
//初始化 DOM 對象
//一、對一些標籤進行事件綁定/屬性的特殊處理
//二、對 DOM 對象內部屬性進行初始化
setInitialProperties(domElement, type, props, rootContainerInstance);
//能夠 foucus 的節點返回autoFocus的值,不然返回 false
return shouldAutoFocusHostComponent(type, props);
}
複製代碼
解析:
(1) 執行setInitialProperties()
,對一些標籤進行事件綁定/屬性的特殊處理,而且對DOM
對象內部屬性進行初始化
(2) 執行shouldAutoFocusHostComponent()
,能夠foucus
的節點會返回autoFocus
的值,不然返回false
8、setInitialProperties
做用:
初始化DOM
對象
源碼:
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): void {
//判斷是不是自定義的 DOM 標籤
const isCustomComponentTag = isCustomComponent(tag, rawProps);
//刪除了 dev 代碼
// TODO: Make sure that we check isMounted before firing any of these events.
//確保在觸發這些監聽器觸發之間,已經初始化了 event
let props: Object;
switch (tag) {
case 'iframe':
case 'object':
case 'embed':
//load listener
//React 自定義的綁定事件,暫時跳過
trapBubbledEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'video':
case 'audio':
// Create listener for each media event
//初始化 media 標籤的監聽器
// export const mediaEventTypes = [
// TOP_ABORT, //abort
// TOP_CAN_PLAY, //canplay
// TOP_CAN_PLAY_THROUGH, //canplaythrough
// TOP_DURATION_CHANGE, //durationchange
// TOP_EMPTIED, //emptied
// TOP_ENCRYPTED, //encrypted
// TOP_ENDED, //ended
// TOP_ERROR, //error
// TOP_LOADED_DATA, //loadeddata
// TOP_LOADED_METADATA, //loadedmetadata
// TOP_LOAD_START, //loadstart
// TOP_PAUSE, //pause
// TOP_PLAY, //play
// TOP_PLAYING, //playing
// TOP_PROGRESS, //progress
// TOP_RATE_CHANGE, //ratechange
// TOP_SEEKED, //seeked
// TOP_SEEKING, //seeking
// TOP_STALLED, //stalled
// TOP_SUSPEND, //suspend
// TOP_TIME_UPDATE, //timeupdate
// TOP_VOLUME_CHANGE, //volumechange
// TOP_WAITING, //waiting
// ];
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement);
}
props = rawProps;
break;
case 'source':
//error listener
trapBubbledEvent(TOP_ERROR, domElement);
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
//error listener
trapBubbledEvent(TOP_ERROR, domElement);
//load listener
trapBubbledEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'form':
//reset listener
trapBubbledEvent(TOP_RESET, domElement);
//submit listener
trapBubbledEvent(TOP_SUBMIT, domElement);
props = rawProps;
break;
case 'details':
//toggle listener
trapBubbledEvent(TOP_TOGGLE, domElement);
props = rawProps;
break;
case 'input':
//在 input 對應的 DOM 節點上新建_wrapperState屬性
ReactDOMInputInitWrapperState(domElement, rawProps);
//淺拷貝value/checked等屬性
props = ReactDOMInputGetHostProps(domElement, rawProps);
//invalid listener
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
//初始化 onChange listener
//https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral
//暫時跳過
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
//dev 環境下
//一、判斷<option>標籤的子節點是不是 number/string
//二、判斷是否正確設置defaultValue/value
ReactDOMOptionValidateProps(domElement, rawProps);
//獲取 option 的 child
props = ReactDOMOptionGetHostProps(domElement, rawProps);
break;
case 'select':
//在 select 對應的 DOM 節點上新建_wrapperState屬性
ReactDOMSelectInitWrapperState(domElement, rawProps);
//設置<select>對象屬性
props = ReactDOMSelectGetHostProps(domElement, rawProps);
//invalid listener
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
//初始化 onChange listener
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
//在 textarea 對應的 DOM 節點上新建_wrapperState屬性
ReactDOMTextareaInitWrapperState(domElement, rawProps);
//設置 textarea 內部屬性
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
//invalid listener
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
//初始化 onChange listener
ensureListeningTo(rootContainerElement, 'onChange');
break;
default:
props = rawProps;
}
//判斷新屬性,好比 style 是否正確賦值
assertValidProps(tag, props);
//設置初始的 DOM 對象屬性
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
//對特殊的 DOM 標籤進行最後的處理
switch (tag) {
case 'input':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
//
track((domElement: any));
ReactDOMInputPostMountWrapper(domElement, rawProps, false);
break;
case 'textarea':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
ReactDOMTextareaPostMountWrapper(domElement, rawProps);
break;
case 'option':
ReactDOMOptionPostMountWrapper(domElement, rawProps);
break;
case 'select':
ReactDOMSelectPostMountWrapper(domElement, rawProps);
break;
default:
if (typeof props.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
//初始化 onclick 事件,以便兼容Safari移動端
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
}
複製代碼
解析:
(1) 判斷是否 是自定義的DOM
標籤,執行isCustomComponent()
,返回true/false
isCustomComponent()
:
function isCustomComponent(tagName: string, props: Object) {
//通常自定義標籤的命名規則是帶`-`的
if (tagName.indexOf('-') === -1) {
//https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/is
return typeof props.is === 'string';
}
//如下的是SVG/MathML的標籤屬性
switch (tagName) {
// These are reserved SVG and MathML elements.
// We don't mind this whitelist too much because we expect it to never grow.
// The alternative is to track the namespace in a few places which is convoluted.
// https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts
case 'annotation-xml':
case 'color-profile':
case 'font-face':
case 'font-face-src':
case 'font-face-uri':
case 'font-face-format':
case 'font-face-name':
case 'missing-glyph':
return false;
default:
return true;
}
}
複製代碼
(2) 而後是對一些標籤,進行一些額外的處理,如初始化特殊的事件監聽、初始化特殊的屬性(通常的標籤是沒有的)等
(3) 看下對<input>
標籤的處理:
① 執行ReactDOMInputInitWrapperState()
,在<input>
對應的DOM
節點上新建_wrapperState
屬性
ReactDOMInputInitWrapperState()
:
//在 input 對應的 DOM 節點上新建_wrapperState屬性
export function initWrapperState(element: Element, props: Object) {
//刪除了 dev 代碼
const node = ((element: any): InputWithWrapperState);
//Input 的默認值
const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
//在 input 對應的 DOM 節點上新建_wrapperState屬性
node._wrapperState = {
//input 有 radio/checkbox 類型,checked 即判斷單/多選框是否被選中
initialChecked:
props.checked != null ? props.checked : props.defaultChecked,
//input 的初始值,優先選擇 value,其次 defaultValue
initialValue: getToStringValue(
props.value != null ? props.value : defaultValue,
),
//radio/checkbox
//若是type 爲 radio/checkbox 的話,看 checked 有沒有被選中
//若是是其餘 type 的話,則看 value 是否有值
controlled: isControlled(props),
};
}
export function getToStringValue(value: mixed): ToStringValue {
switch (typeof value) {
case 'boolean':
case 'number':
case 'object':
case 'string':
case 'undefined':
return value;
default:
// function, symbol are assigned as empty strings
return '';
}
}
function isControlled(props) {
const usesChecked = props.type === 'checkbox' || props.type === 'radio';
return usesChecked ? props.checked != null : props.value != null;
}
複製代碼
② 執行ReactDOMInputGetHostProps()
,淺拷貝、初始化value/checked
等屬性
getHostProps()
:
//淺拷貝value/checked等屬性
export function getHostProps(element: Element, props: Object) {
const node = ((element: any): InputWithWrapperState);
const checked = props.checked;
//淺拷貝
const hostProps = Object.assign({}, props, {
defaultChecked: undefined,
defaultValue: undefined,
value: undefined,
checked: checked != null ? checked : node._wrapperState.initialChecked,
});
return hostProps;
}
複製代碼
③ 執行ensureListeningTo()
,初始化onChange listener
(4) 看下對<option>
標籤的處理:
① 執行ReactDOMOptionValidateProps()
,在 dev 環境下:
[1] 判斷
ReactDOMOptionValidateProps()
:
export function validateProps(element: Element, props: Object) {
if (__DEV__) {
// This mirrors the codepath above, but runs for hydration too.
// Warn about invalid children here so that client and hydration are consistent.
// TODO: this seems like it could cause a DEV-only throw for hydration
// if children contains a non-element object. We should try to avoid that.
if (typeof props.children === 'object' && props.children !== null) {
React.Children.forEach(props.children, function(child) {
if (child == null) {
return;
}
if (typeof child === 'string' || typeof child === 'number') {
return;
}
if (typeof child.type !== 'string') {
return;
}
if (!didWarnInvalidChild) {
didWarnInvalidChild = true;
warning(
false,
'Only strings and numbers are supported as <option> children.',
);
}
});
}
// TODO: Remove support for `selected` in <option>.
if (props.selected != null && !didWarnSelectedSetOnOption) {
warning(
false,
'Use the `defaultValue` or `value` props on <select> instead of ' +
'setting `selected` on <option>.',
);
didWarnSelectedSetOnOption = true;
}
}
}
複製代碼
② 執行ReactDOMOptionGetHostProps()
,獲取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
上,請參考:
registrationNameModules
:
⑤ 不是上述狀況的話,則setValueForProperty()
,爲DOM
節點設置屬性值(這個 function 太長了,暫時跳過)
(9) 最後又是一串switch...case
,對特殊的DOM
標籤進行最後的處理,瞭解下就好
9、shouldAutoFocusHostComponent
做用:
能夠foucus
的節點會返回autoFocus
的值,不然返回false
源碼:
//能夠 foucus 的節點返回autoFocus的值,不然返回 false
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
//能夠 foucus 的節點返回autoFocus的值,不然返回 false
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
return !!props.autoFocus;
}
return false;
}
複製代碼
解析:
比較簡單
七
到九
是對finalizeInitialChildren()
及其內部function
的解析,本文也到此結束了,最後放上 GitHub
10、GitHubReactFiberCompleteWork.js
:
github.com/AttackXiaoJ…
ReactDOMHostConfig.js
:
github.com/AttackXiaoJ…
ReactDOMComponent.js
:
github.com/AttackXiaoJ…
(完)