上一章讲解了 # React源码解析系列(五) -- reconcileChildren的解读,我们知道React
在处理新的newChild
和老的fiber
的时候的处理机制,也学习了他的diff
过程以及diff
策略,那么这一章就要讲一讲处理后的fiber
链表树变成真实的dom
的过程,当然他会在commit
阶段的commitMatutionEffects
函数里面去进行实际的dom
操作。那么我们一起来看看吧。
beginWork
函数执行完毕之后,我们就回去执行completeUnitOfWork
函数,那么我也给这一流程画了一个简略的图:
completeWork
completeWork
函数太长,截取部分关键,这一部分是根据workInProgress.tag
来进行区分组件进行处理的,但是关键在于HostComponent
,这是处理标准基础组件的函数,我们依次往下看这个函数里面到底干了啥。
HostComponent
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// 非初始化,调updateHostComponent更新
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
// 处理ref
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
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.
return null;
}
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 whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext,
)
) {
// If changes to the hydrated node need to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
// 创建虚拟dom实例
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 把所有子元素绑定到父级上面去
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// 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,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
return null;
}
以上代码区分于初始化与更新阶段。
- 初始化阶段,根据
createInstatnce
函数去创建dom
实例,绑定父级相关的所有子元素,最后处理子元素的内部情况。 - 更新的情况下,直接调用
updateHostComponent
去更新。
createInstance
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev = ((hostContext: any): HostContextDev);
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
const string = '' + props.children;
const ownAncestorInfo = updatedAncestorInfo(
hostContextDev.ancestorInfo,
type,
);
validateDOMNesting(null, string, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
} else {
parentNamespace = ((hostContext: any): HostContextProd);
}
// 创建element节点
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
// 给元素加上属性
updateFiberProps(domElement, props);
return domElement;
}
这里我们看到了var domElement = ....,是不是很熟悉,这里就是针对每一个闭合的基础标签进行创建真实dom标签的处理的,我们继续看看createElement的具体实现。
createElement
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.
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
rootContainerElement,
);
let domElement: Element;
let namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
if (namespaceURI === HTML_NAMESPACE) {
// 处理script
if (type === 'script') {
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') {
domElement = ownerDocument.createElement(type, {is: props.is});
} else {
domElement = ownerDocument.createElement(type);
if (type === 'select') {
const node = ((domElement: any): HTMLSelectElement);
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
node.size = props.size;
}
}
}
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
...
return domElement;
}
在createElement
方法中,react
会根据不同的标签进行创建dom,比如你是一个script
标签,那就创建一个div标签包裹起来。如果不是上述特殊标签,则直接通过ownerDocument.createElement(type)
创建。
updateFiberProps
经过上面处理的标签,仅仅只是一个空标签,除此还有属性和子元素,那么updateFiberProps
则是标签属性和子元素给当前标签添上的。
export function updateFiberProps(
node: Instance | TextInstance | SuspenseInstance,
props: Props,
): void {
(node: any)[internalPropsKey] = props;
}
finalizeInitialChildren
最后通过finalizeInitialChildren
处理加工子元素,那么子元素可能为单节点
,可能为多节点
,那么我们看看finalizeInitialChildren
的具体实现。
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
// 处理属性
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
}
上述代码本质就是调用setInitialProperties
,我们继续看看他的源码实现。
// packages/react-dom/src/client/ReactDOMComponent.js
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): void {
const isCustomComponentTag = isCustomComponent(tag, rawProps);
if (__DEV__) {
validatePropertiesInDevelopment(tag, rawProps);
}
// TODO: Make sure that we check isMounted before firing any of these events.
let props: Object;
switch (tag) {
case 'dialog':
listenToNonDelegatedEvent('cancel', domElement);
listenToNonDelegatedEvent('close', domElement);
props = rawProps;
break;
case 'iframe':
case 'object':
case 'embed':
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the load event.
listenToNonDelegatedEvent('load', domElement);
props = rawProps;
break;
case 'video':
case 'audio':
// We listen to these events in case to ensure emulated bubble
// listeners still fire for all the media events.
for (let i = 0; i < mediaEventTypes.length; i++) {
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
}
props = rawProps;
break;
case 'source':
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the error event.
listenToNonDelegatedEvent('error', domElement);
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
// We listen to these events in case to ensure emulated bubble
// listeners still fire for error and load events.
listenToNonDelegatedEvent('error', domElement);
listenToNonDelegatedEvent('load', domElement);
props = rawProps;
break;
case 'details':
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the toggle event.
listenToNonDelegatedEvent('toggle', domElement);
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
// 进入事件监听注册流程
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
props = ReactDOMOptionGetHostProps(domElement, rawProps);
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
// 进入事件监听注册流程
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
// 进入事件监听注册流程
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
default:
props = rawProps;
}
// 验证props合法性
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;
}
}
这里的源码处理的事情也是简单,也就是根据当前的闭合标签类型去做对应的处理,最后验证props
,并且添加最终确定的props
。
setInitialDOMProperties
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设置的样式,则用setValueForStyles给dom添加上
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);
}
}
// Relies on `updateStylesByID` not mutating `styleUpdates`.
setValueForStyles(domElement, nextProp);
} 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
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 (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (!enableEagerRootListeners) {
// 进入事件监听注册流程
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
setInitialDOMProperties
代码中处理的事情,就是遍历当前props
,根据key
的不同去做不同的处理。
- 如果
key
为STYLE
,则通过setValueForStyles
去处理属性设置。 - 如果
key
为CHILDREN
,则通过setTextContent
去处理文本内容。
setValueForStyles
export function setValueForStyles(node, styles) {
const style = node.style;
for (let styleName in styles) {
if (!styles.hasOwnProperty(styleName)) {
continue;
}
const isCustomProperty = styleName.indexOf('--') === 0;
if (__DEV__) {
if (!isCustomProperty) {
warnValidStyle(styleName, styles[styleName]);
}
}
// 获取属性样式值
const styleValue = dangerousStyleValue(
styleName,
styles[styleName],
isCustomProperty,
);
if (styleName === 'float') {
styleName = 'cssFloat';
}
// 覆盖原来的属性
if (isCustomProperty) {
style.setProperty(styleName, styleValue);
} else {
style[styleName] = styleValue;
}
}
}
setTextContent
const setTextContent = function(node: Element, text: string): void {
if (text) {
const firstChild = node.firstChild;
if (
firstChild &&
firstChild === node.lastChild &&
firstChild.nodeType === TEXT_NODE
) {
firstChild.nodeValue = text;
return;
}
}
// 设置真实文本内容
node.textContent = text;
};
所以走到这里基本上一个标签就创建完毕了,如若不明白,我简单的画一个图如下:
总结
那么这一章到这里就结束了,本文也就讲了初始化时候的经历过createInstance
、createElement
等一些列操作之后变成真实dom
的经过,当然了,这里并没有讲updateHostComponent
的实现,其实在源码里面,updateHostComponent
是一个不断被重构的函数,但是最后都是执行finalizeInitialChildren
,有兴趣的朋友可以去看看哈。那么下一章也就是讲解一下整个React
的commit
过程,直通车 >> React源码解析系列(七) -- commit阶段主要流程