React 源码讲解第 2 节-入口 API 之 ReactDOM.render
前言
在 node_modules 目录中找到 react-dom 包,打开 cjs 文件夹。
打开 react-dom.development.js 文件。
定位到 25005 行,找到导出的 render 方法,源码如下。
exports.render = render;
找到 render 的实现方法,源码如下。
function render(element, container, callback) {
if (!isValidContainer(container)) {
{
throw Error( "Target container is not a DOM element." );
}
}
{
var isModernRoot = isContainerMarkedAsRoot(container) && container._reactRootContainer === undefined;
if (isModernRoot) {
error('You are calling ReactDOM.render() on a container that was previously ' + 'passed to ReactDOM.createRoot(). This is not supported. ' + 'Did you mean to call root.render(element)?');
}
}
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}
源码分析
1. render 参数
通过 demo 中的 ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') )
,结合源码,可以看出 render 方法中对应的参数。
第一个参数 element 指的就是 <App />
。
第二个参数 container 指的就是根节点 document.getElementById('root')
。
第三个参数 callback 很少用到,这里不展开。
2. 判断是否为有效节点
isValidContainer(container)
是判断父节点 container
是否为有效节点,以下任意节点都是有效节点。
var ELEMENT_NODE = 1;//元素节点
var TEXT_NODE = 3;//文本节点
var COMMENT_NODE = 8;//注释节点
var DOCUMENT_NODE = 9;//文档,DOM树根节点
var DOCUMENT_FRAGMENT_NODE = 11;//节点片段
3. 判断是否为根节点
isContainerMarkedAsRoot
判断是否为根节点。
4. 初始化根节点(创建 FiberRoot 及 RootFiber)
4.1 找到 legacyRenderSubtreeIntoContainer
的实现方法,源码如下。
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
{
topLevelUpdateWarnings(container);
warnOnInvalidCallback$1(callback === undefined ? null : callback, 'render');
} // TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
var root = container._reactRootContainer;
var fiberRoot;
if (!root) {
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
var originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
} // Initial mount should not be batched.
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
var _originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
_originalCallback.call(instance);
};
} // Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
通过以上代码可以看到该方法中的参数如下。
第一个参数 parentComponent 为 null。
第二个参数 children 指的就是 <App />
。
第三个参数 container 指的就是根节点 document.getElementById('root')
。
第四个参数 forceHydrate 判断是否为服务端渲染,默认为 false。
第五个参数 callback 回调函数。
首次加载时,root 肯定为 underfined。所以 root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
4.2 找到 legacyCreateRootFromDOMContainer(container, forceHydrate)
实现方法,源码如下。
function legacyCreateRootFromDOMContainer(container, forceHydrate) {
var shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // First clear any existing content.
if (!shouldHydrate) {
var warned = false;
var rootSibling;
while (rootSibling = container.lastChild) {
{
if (!warned && rootSibling.nodeType === ELEMENT_NODE && rootSibling.hasAttribute(ROOT_ATTRIBUTE_NAME)) {
warned = true;
error('render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.');
}
}
container.removeChild(rootSibling);
}
}
{
if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
warnedAboutHydrateAPI = true;
warn('render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + 'will stop working in React v17. Replace the ReactDOM.render() call ' + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.');
}
}
return createLegacyRoot(container, shouldHydrate ? {
hydrate: true
} : undefined);
}
参数 container 为根节点, forceHydrate 为 false。
看核心代码 createLegacyRoot(container, shouldHydrate ? { hydrate: true } : undefined);
其内部实例化了 ReactDOMBlockingRoot(container, LegacyRoot, options)
。该实例的属性 _internalRoot 等于 createRootImpl(container, tag, options)。可以看到最后是实例化了 FiberRootNode(containerInfo, tag, hydrate)
构造函数,也就是 fiberRoot 。
5. 创建 expirationTime 和 update
回到 updateContainer(children, fiberRoot, parentComponent, callback)
方法。源码如下。
function updateContainer(element, container, parentComponent, callback) {
{
onScheduleRoot(container, element);
}
var current$1 = container.current;
var currentTime = requestCurrentTimeForUpdate();
{
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfUnmockedScheduler(current$1);
warnIfNotScopedWithMatchingAct(current$1);
}
}
var suspenseConfig = requestCurrentSuspenseConfig();
var expirationTime = computeExpirationForFiber(currentTime, current$1, suspenseConfig);
var context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
{
if (isRendering && current !== null && !didWarnAboutNestedUpdates) {
didWarnAboutNestedUpdates = true;
error('Render methods should be a pure function of props and state; ' + 'triggering nested component updates from render is not allowed. ' + 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + 'Check the render method of %s.', getComponentName(current.type) || 'Unknown');
}
}
var update = createUpdate(expirationTime, suspenseConfig); // Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {
element: element
};
callback = callback === undefined ? null : callback;
if (callback !== null) {
{
if (typeof callback !== 'function') {
error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
}
}
update.callback = callback;
}
enqueueUpdate(current$1, update);
scheduleWork(current$1, expirationTime);
return expirationTime;
}
该方法中创建了一个更新。var update = createUpdate(expirationTime, suspenseConfig)
然后将更新加入了任务列队。enqueueUpdate(current$1, update)
将当前节点和过期时间加入调度器中。 scheduleWork(current$1, expirationTime)
总结
- 在 render 方法的第一个参数 element 的基础上创建 fiberRoot 节点。
- 通过 expirationTime 过期时间创建一个更新
createUpdate(expirationTime, suspenseConfig)
。 - 将当前元素
current$1
和更新update
作为参数加入更新队列enqueueUpdate(current$1, update)
。 - 将当前元素
current$1
和过期时间expirationTime
作为参数进入调度器scheduleWork(current$1, expirationTime)
。