前言
在上篇 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