前言
在上篇 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()
的解析,本文也到此結束了,最後放上 GitHub
function
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