React 初次渲染的时候无非会经历如下四个步骤:
ReactElement 元素的创建,以及它如何表达嵌套在上个章节中也已阐述。React 的难点就在于如何进行数据处理,并且保持高效,我们暂且忽略它。秉持由浅入深的原则进行学习。
想要从 ReactElement 元素的层层嵌套结构中解析出 DOM,能直接想到的办法就是使用递归。在遇到每一层的时候根据元素类型创建不同的 DOM 元素,最后将这一个庞大的 DOM 元素树插入到真实的 DOM 中。
这些功能,都集中在:
packages/react-dom/src/client/ReactDOMComponent.js
createElement
/**
* 创建元素
* @param type
* @param props
* @param rootContainerElement
* @param parentNamespace
* @returns {Element}
*/
export function createElement(
type: string,
props: Object,
rootContainerElement: Element | Document,
parentNamespace: string,
): Element {
let isCustomComponentTag;
// 我们在它们的父容器的命名空间中创建标记,除了 HTML 标签没有命名空间。
// getOwnerDocumentFromRootContainer 方法用来获取 document
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
rootContainerElement,
);
let domElement: Element;
let namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
if (namespaceURI === HTML_NAMESPACE) {
if (type === 'script') {
// 通过 .innerHTML 创建脚本,这样它的 “parser-inserted” 标志就被设置为true,而且不会执行
const div = ownerDocument.createElement('div');
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
// 这将保证生成一个脚本元素。
const firstChild = ((div.firstChild: any): HTMLScriptElement);
domElement = div.removeChild(firstChild);
} else if (typeof props.is === 'string') {
// $FlowIssue `createElement` 应该为 Web Components 更新
domElement = ownerDocument.createElement(type, {is: props.is});
} else {
// 因为 Firefox bug,分离 else 分支,而不是使用 `props.is || undefined`
// 参见 https://github.com/facebook/react/pull/6896
// 和 https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
// 通常在 `setInitialDOMProperties` 中分配属性,
// 但是 `select` 上的 `multiple` 和 `size` 属性需要在 `option` 被插入之前添加。
// 这可以防止:
// 一个错误,其中 `select` 不能滚动到正确的选项,因为单一的 `select` 元素自动选择第一项 #13222
// 一个 bug,其中 `select` 将第一个项目设置为 selected,而忽略 `size` 属性 #14239
// 参见 https://github.com/facebook/react/issues/13222
// 和 https://github.com/facebook/react/issues/14239
if (type === 'select') {
const node = ((domElement: any): HTMLSelectElement);
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
// 设置大于 1 的 size 会使 select 的行为类似于 `multiple=true`,其中可能没有选择任何选项。
// select 只有在“单一选择模式”下才需要这样做。
node.size = props.size;
}
}
}
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
return domElement;
}复制代码
createElement
方法会根据不同的类型创建不同的 DOM 元素。值得指出的是,它不仅支持原生的 html 标签,还支持 WebComponent。
createTextNode
export function createTextNode(
text: string,
rootContainerElement: Element | Document,
): Text {
return getOwnerDocumentFromRootContainer(rootContainerElement).createTextNode(
text,
);
}复制代码
createTextNode
方法创建了一个文本节点
setInitialProperties
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): void {
const isCustomComponentTag = isCustomComponent(tag, rawProps);
// TODO: Make sure that we check isMounted before firing any of these events.
let props: Object;
switch (tag) {
case 'iframe':
case 'object':
trapBubbledEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'video':
case 'audio':
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement);
}
props = rawProps;
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement);
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_LOAD, domElement);
props = rawProps;
break;
case 'form':
trapBubbledEvent(TOP_RESET, domElement);
trapBubbledEvent(TOP_SUBMIT, domElement);
props = rawProps;
break;
case 'details':
trapBubbledEvent(TOP_TOGGLE, domElement);
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
props = ReactDOMOptionGetHostProps(domElement, rawProps);
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
default:
props = rawProps;
}
assertValidProps(tag, props);
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
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.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
}复制代码
trapBubbledEvent
为元素添加事件。
setInitialDOMProperties
/**
* 设置初始 DOM 属性
* @param tag
* @param domElement
* @param rootContainerElement
* @param nextProps
* @param isCustomComponentTag
*/
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
// 当前遍历的属性值
const nextProp = nextProps[propKey];
// 设置默认 style 标签的属性
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
// 使用 node.style['cssFloat'] 这样的对象语法来为节点设置样式
// 依赖于 `updateStylesByID` 而不是 `styleUpdates`.
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) { // 单独处理 html 字符串
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml); // 设置 innerHTML
}
} else if (propKey === CHILDREN) { // 处理 children 属性
if (typeof nextProp === 'string') {
// 避免在文本为空时设置初始文本内容。
// 在IE11中,在 <textarea> 上设置 textContent
// 将导致占位符(placeholder)不显示在 <textarea> 中,直到它再次被 focus 和 blur。
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
setTextContent(domElement, nextProp); // 设置文本
}
} 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).
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey); // 事件监听
}
} else if (nextProp != null) {
// 设置属性值
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}复制代码
setInitialDOMProperties 用于设置 DOM 元素初始的属性。
- 首先,它会遍历所有的属性,过滤掉非自身的属性
- 处理
dangerouslySetInnerHTML
属性,该属性接收一个{ __html: '' }
格式的对象,因此这里取出其__html
,使用setInnerHTML
方法将其设置为innerHTML
- 处理字符串和数字文本,使用文本节点的
.nodeValue
属性设置其文本内容 - 处理事件处理函数属性
- 使用
setValueForProperty
方法设置普通属性的值
其中有意思的是 React 处理 html 字符串的方式,将其放置在了 svg 标签中。为何如此处理?
const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(
node: Element,
html: string,
): void {
// IE 没有针对 SVG 节点的 innerHTML,
// 因此我们将新标记注入临时节点,然后将子节点移动到目标节点
// 也就是说不能通过 SVG 的 innerHTML 属性直接赋值
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;
}
});复制代码
updateProperties
/**
* 应用 diff,更新属性
* @param domElement
* @param updatePayload
* @param tag
* @param lastRawProps
* @param nextRawProps
*/
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// 更新名称前的选中状态
// 在更新过程中,可能会有多个被选中项
// 当一个选中的单选框改变名称时,浏览器会使其他的单选框的选中状态为 false
if (
tag === 'input' &&
nextRawProps.type === 'radio' &&
nextRawProps.name != null
) {
ReactDOMInputUpdateChecked(domElement, nextRawProps); // 更新 radio 的 Checked 状态
}
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// 应用 diff.
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// TODO: Ensure that an update gets scheduled if any of the special props
// changed.
switch (tag) {
case 'input':
// 在更新 props 之后更新 input 的包装器。
// 这必须发生在 `updateDOMProperties` 之后。
// 否则,HTML5 输入验证将发出警告并阻止新值的分配。
ReactDOMInputUpdateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
break;
case 'select':
// <select> 的值更新需要在 <option> 子元素 reconciliation 之后发生
ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
break;
}
}复制代码
updateDOMProperties
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}复制代码
由于当前元素已经存在于 dom 中了,因此只需要根据传入的数据进行更新就行了。
diffProperties
/**
* 计算两个对象之间的差异。
* 为什么需要更新,无非三种情况:
* 1. 之前有,之后没有,需要删除
* 2. 之前没有,之后有,需要新增
* 3. 前后值不一样,需要更新
*
* 但在 react 的实现中,新增和更新的逻辑都是一样,因此可以合并:
* 1. 之前有,之后没有,需要删除
* 2. 前后值不一样,需要更新
*
* @param domElement
* @param tag
* @param lastRawProps
* @param nextRawProps
* @param rootContainerElement
* @returns {Array<*>}
*/
export function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
rootContainerElement: Element | Document,
): null | Array<mixed> {
if (__DEV__) {
validatePropertiesInDevelopment(tag, nextRawProps);
}
let updatePayload: null | Array<any> = null;
let lastProps: Object;
let nextProps: Object;
switch (tag) {
case 'input':
lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'option':
lastProps = ReactDOMOptionGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMOptionGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'select':
lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'textarea':
lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (
typeof lastProps.onClick !== 'function' &&
typeof nextProps.onClick === 'function'
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
assertValidProps(tag, nextProps);
let propKey;
let styleName;
let styleUpdates = null; // 用以记录需要更新的样式,1. 需要清除的样式 2. 新增的或者值改变的样式
for (propKey in lastProps) {
// nextProps 有此属性不处理
// lastProps 没有此属性不处理
// lastProps 此属性值为 null 或 undefined 不处理
// 也就是说,只处理 lastProps 有,但 nextProps 没有的,且 lastProps 中值不为 null 或 undefined 的
// 更直白点说就是重置需要删除的属性
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
continue;
}
if (propKey === STYLE) { // 处理 style 属性
const lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = ''; // 清除之前的样式
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
// Noop. This is handled by the clear text mechanism.
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// 无操作。无论如何,它都不能在更新上工作。
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// 这是一个特例。如果任何侦听器更新,
// 我们需要确保 "current" props 指针得到更新,
// 因此我们需要一个提交来更新此元素。
if (!updatePayload) {
updatePayload = [];
}
} else {
// 对于所有其他已删除的属性,我们将其添加到队列中。相反,我们在提交阶段使用白名单。
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
// nextProps 没有的属性不添加到更新
// nextProp 和 lastProp 相等不处理
// nextProp 和 lastProp 都为 null 或 undefined 不处理
// 也就是说值处理 nextProps 有的,值不为 null 或 undefined,且 nextProp 和 lastProp 的情况
// 更直白点说就是更新值发生改变的属性
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
continue;
}
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if ( // lastProp 有这个属性,并且 nextProp 不存在或者没有此属性。
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = ''; // 置空样式属性值
}
}
// 更新自 `lastProp` 以来更改的样式。
for (styleName in nextProp) {
// nextProp 存在的样式属性,且前后值不一样
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else { // 以前不存在 style 属性
// 依赖于 `updateStylesByID` 而不是 `styleUpdates`.
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates); // 等同于 updatePayload.push(propKey, null);
}
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined; // 取出 .__html 中存储的 html 字符串
const lastHtml = lastProp ? lastProp[HTML] : undefined; // 取出 .__html 中存储的 html 字符串
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml); // 需要更新的 html
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
}
} else if (propKey === CHILDREN) {
// 处理纯文本
if (
lastProp !== nextProp &&
(typeof nextProp === 'string' || typeof nextProp === 'number')
) {
(updatePayload = updatePayload || []).push(propKey, '' + nextProp);
}
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
// We eagerly listen to this even though we haven't committed yet.
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
// 这是一个特例。如果任何侦听器更新,
// 我们需要确保 "current" props 指针得到更新,
// 因此我们需要一个提交来更新此元素。
updatePayload = [];
}
} else {
// 对于任何其他属性,我们总是将其添加到队列中,然后在提交期间使用白名单过滤掉。
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
if (__DEV__) {
validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
}
(updatePayload = updatePayload || []).push(STYLE, styleUpdates); // 添加样式更新
}
return updatePayload;
}复制代码
diffHydratedProperties
diffHydratedText
计算文本节点之间的差异
/**
* 计算文本节点之间的差异。
* @param textNode
* @param text
* @returns {boolean}
*/
function diffHydratedText(textNode: Text, text: string): boolean {
const isDifferent = textNode.nodeValue !== text;
return isDifferent;
}复制代码
restoreControlledState
/**
* 恢复受控状态
* @param domElement
* @param tag
* @param props
*/
export function restoreControlledState(
domElement: Element,
tag: string,
props: Object,
): void {
switch (tag) {
case 'input':
ReactDOMInputRestoreControlledState(domElement, props);
return;
case 'textarea':
ReactDOMTextareaRestoreControlledState(domElement, props);
return;
case 'select':
ReactDOMSelectRestoreControlledState(domElement, props);
return;
}
}复制代码
遗留问题
上面创建的元素只是创建了一个,如何创建一个 DOM 树?