packages -> react-dom -> src -> client -> ReactDOM.js
然后我们将代码定位到第632
行,可以看到ReactDOM
对象包含了很多我们可能使用过的方法,例如render
、createPortal
、findDOMNode
,hydrate
和unmountComponentAtNode
等。本文中我们暂且只关心render
方法,但为了方便对比,也可以简单看下hydrate
方法:
const ReactDOM: Object = {
…
/**
-
服务端渲染
-
@param element 表示一个ReactNode,可以是一个ReactElement对象
-
@param container 需要将组件挂载到页面中的DOM容器
-
@param callback 渲染完成后需要执行的回调函数
*/
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
invariant(
isValidContainer(container),
‘Target container is not a DOM element.’,
);
…
// TODO: throw or warn if we couldn’t hydrate?
// 注意第一个参数为null,第四个参数为true
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback,
);
},
/**
-
客户端渲染
-
@param element 表示一个ReactElement对象
-
@param container 需要将组件挂载到页面中的DOM容器
-
@param callback 渲染完成后需要执行的回调函数
*/
render(
element: React$Element,
container: DOMContainer,
callback: ?Function,
) {
invariant(
isValidContainer(container),
‘Target container is not a DOM element.’,
);
…
// 注意第一个参数为null,第四个参数为false
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
…
};
发现没,render
方法的第一个参数就是我们在上篇文章中讲过的ReactElement
对象,所以说上篇文章的内容就是为了在这里打下基础的,便于我们对参数的理解。事实上,在源码中几乎所有方法参数中的element
字段均可以传入一个ReactElement
实例,这个实例就是通过 Babel 编译器在编译过程中使用React.createElement
方法得到的。接下来在render
方法中调用legacyRenderSubtreeIntoContainer
来正式进入渲染流程,不过这里需要留意一下的是,render
方法和hydrate
方法在执行legacyRenderSubtreeIntoContainer
时,第一个参数的值均为null
,第四个参数的值恰好相反。
然后将代码定位到第570
行,进入legacyRenderSubtreeIntoContainer
方法的具体实现:
/**
-
开始构建FiberRoot和RootFiber,之后开始执行更新任务
-
@param parentComponent 父组件,可以把它当成null值来处理
-
@param children ReactDOM.render()或者ReactDOM.hydrate()中的第一个参数,可以理解为根组件
-
@param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
-
@param forceHydrate 表示是否融合,用于区分客户端渲染和服务端渲染,render方法传false,hydrate方法传true
-
@param callback ReactDOM.render()或者ReactDOM.hydrate()中的第三个参数,组件渲染完成后需要执行的回调函数
-
@returns {*}
*/
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
…
// TODO: Without any
type, Flow says "Property cannot be accessed on any
// member of interp type." Whyyyyyy.
// 在第一次执行的时候,container上是肯定没有_reactRootContainer属性的
// 所以第一次执行时,root肯定为undefined
let root: _ReactSyncRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// Initial mount
// 首次挂载,进入当前流程控制中,container._reactRootContainer指向一个ReactSyncRoot实例
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// root表示一个ReactSyncRoot实例,实例中有一个_internalRoot方法指向一个fiberRoot实例
fiberRoot = root._internalRoot;
// callback表示ReactDOM.render()或者ReactDOM.hydrate()中的第三个参数
// 重写callback,通过fiberRoot去找到其对应的rootFiber,然后将rootFiber的第一个child的stateNode作为callback中的this指向
// 一般情况下我们很少去写第三个参数,所以可以不必关心这里的内容
if (typeof callback === ‘function’) {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
// 对于首次挂载来说,更新操作不应该是批量的,所以会先执行unbatchedUpdates方法
// 该方法中会将executionContext(执行上下文)切换成LegacyUnbatchedContext(非批量上下文)
// 切换上下文之后再调用updateContainer执行更新操作
// 执行完updateContainer之后再将executionContext恢复到之前的状态
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// 不是首次挂载,即container._reactRootContainer上已经存在一个ReactSyncRoot实例
fiberRoot = root._internalRoot;
// 下面的控制语句和上面的逻辑保持一致
if (typeof callback === ‘function’) {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
// 对于非首次挂载来说,是不需要再调用unbatchedUpdates方法的
// 即不再需要将executionContext(执行上下文)切换成LegacyUnbatchedContext(非批量上下文)
// 而是直接调用updateContainer执行更新操作
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
上面代码的内容稍微有些多,咋一看可能不太好理解,我们暂且可以不用着急看完整个函数内容。试想当我们第一次启动运行项目的时候,也就是第一次执行ReactDOM.render
方法的时候,这时去获取container._reactRootContainer
肯定是没有值的,所以我们先关心第一个if
语句中的内容:
if (!root) {
// Initial mount
// 首次挂载,进入当前流程控制中,container._reactRootContainer指向一个ReactSyncRoot实例
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
…
}
这里通过调用legacyCreateRootFromDOMContainer
方法将其返回值赋值给container._reactRootContainer
,我们将代码定位到同文件下的第517
行,去看看legacyCreateRootFromDOMContainer
的具体实现:
/**
-
创建并返回一个ReactSyncRoot实例
-
@param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
-
@param forceHydrate 是否需要强制融合,render方法传false,hydrate方法传true
-
@returns {ReactSyncRoot}
*/
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): _ReactSyncRoot {
// 判断是否需要融合
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
// 针对客户端渲染的情况,需要将container容器中的所有元素移除
if (!shouldHydrate) {
let warned = false;
let rootSibling;
// 循环遍历每个子节点进行删除
while ((rootSibling = container.lastChild)) {
…
container.removeChild(rootSibling);
}
}
…
// Legacy roots are not batched.
// 返回一个ReactSyncRoot实例
// 该实例具有一个_internalRoot属性指向fiberRoot
return new ReactSyncRoot(
container,
LegacyRoot,
shouldHydrate
? {
hydrate: true,
-
}
- undefined,
);
}
/**
-
根据nodeType和attribute判断是否需要融合
-
@param container DOM容器
-
@returns {boolean}
*/
function shouldHydrateDueToLegacyHeuristic(container) {
const rootElement = getReactRootElementInContainer(container);
return !!(
rootElement &&
rootElement.nodeType === ELEMENT_NODE &&
rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
);
}
/**
-
根据container来获取DOM容器中的第一个子节点
-
@param container DOM容器
-
@returns {*}
*/
function getReactRootElementInContainer(container: any) {
if (!container) {
return null;
}
if (container.nodeType === DOCUMENT_NODE) {
return container.documentElement;
} else {
return container.firstChild;
}
}
其中在shouldHydrateDueToLegacyHeuristic
方法中,首先根据container
来获取 DOM 容器中的第一个子节点,获取该子节点的目的在于通过节点的nodeType
和是否具有ROOT_ATTRIBUTE_NAME
属性来区分是客户端渲染还是服务端渲染,ROOT_ATTRIBUTE_NAME
位于packages/react-dom/src/shared/DOMProperty.js
文件中,表示data-reactroot
属性。我们知道,在服务端渲染中有别于客户端渲染的是,node
服务会在后台先根据匹配到的路由生成完整的HTML
字符串,然后再将HTML
字符串发送到浏览器端,最终生成的HTML
结构简化后如下:
在客户端渲染中是没有data-reactroot
属性的,因此就可以区分出客户端渲染和服务端渲染。在 React 中的nodeType
主要包含了五种,其对应的值和W3C
中的nodeType
标准是保持一致的,位于与DOMProperty.js
同级的HTMLNodeType.js
文件中:
// 代表元素节点
export const ELEMENT_NODE = 1;
// 代表文本节点
export const TEXT_NODE = 3;
// 代表注释节点
export const COMMENT_NODE = 8;
// 代表整个文档,即document
export const DOCUMENT_NODE = 9;
// 代表文档片段节点
export const DOCUMENT_FRAGMENT_NODE = 11;
经过以上分析,现在我们就可以很容易地区分出客户端渲染和服务端渲染,并且在面试中如果被问到两种渲染模式的区别,我们就可以很轻松地在源码级别上说出两者的实现差异,让面试官眼前一亮。怎么样,到目前为止,其实还是觉得挺简单的吧?
FiberRoot VS RootFiber
在这一小节中,我们将尝试去理解两个比较容易混淆的概念:FiberRoot
和RootFiber
。这两个概念在 React 的整个任务调度过程中起着关键性的作用,如果不理解这两个概念,后续的任务调度过程就是空谈,所以这里也是我们必须要去理解的部分。接下来接着上一小节的内容,继续分析legacyCreateRootFromDOMContainer
方法中的剩余内容,在函数体的结尾返回了一个ReactSyncRoot
实例,我们重新回到ReactDOM.js
文件可以很容易找到ReactSyncRoot
构造函数的具体内容:
/**
-
ReactSyncRoot构造函数
-
@param container DOM容器
-
@param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
-
@param options 配置信息,只有在hydrate时才有值,否则为undefined
-
@constructor
*/
function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}
/**
-
创建并返回一个fiberRoot
-
@param container DOM容器
-
@param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
-
@param options 配置信息,只有在hydrate时才有值,否则为undefined
-
@returns {*}
*/
function createRootImpl(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
) {
// Tag is either LegacyRoot or Concurrent Root
// 判断是否是hydrate模式
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
// 创建一个fiberRoot
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
// 给container附加一个内部属性用于指向fiberRoot的current属性对应的rootFiber节点
markContainerAsRoot(root.current, container);
if (hydrate && tag !== LegacyRoot) {
const doc =
container.nodeType === DOCUMENT_NODE
-
? container
- container.ownerDocument;
eagerlyTrapReplayableEvents(doc);
}
return root;
}
从上述源码中,我们可以看到createRootImpl
方法通过调用createContainer
方法来创建一个fiberRoot
实例,并将该实例返回并赋值到ReactSyncRoot
构造函数的内部成员_internalRoot
属性上。我们继续深入createContainer
方法去探究一下fiberRoot
完整的创建过程,该方法被抽取到与react-dom
包同级的另一个相关的依赖包react-reconciler
包中,然后定位到react-reconciler/src/ReactFiberReconciler.js
的第299
行:
/**
-
内部调用createFiberRoot方法返回一个fiberRoot实例
-
@param containerInfo DOM容器
-
@param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
-
@param hydrate 判断是否是hydrate模式
-
@param hydrationCallbacks 只有在hydrate模式时才可能有值,该对象包含两个可选的方法:onHydrated和onDeleted
-
@returns {FiberRoot}
*/
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
/**
-
创建fiberRoot和rootFiber并相互引用
-
@param containerInfo DOM容器
-
@param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
-
@param hydrate 判断是否是hydrate模式
-
@param hydrationCallbacks 只有在hydrate模式时才可能有值,该对象包含两个可选的方法:onHydrated和onDeleted
-
@returns {FiberRoot}
*/
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 通过FiberRootNode构造函数创建一个fiberRoot实例
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
// 通过createHostRootFiber方法创建fiber tree的根节点,即rootFiber
// 需要留意的是,fiber节点也会像DOM树结构一样形成一个fiber tree单链表树结构
// 每个DOM节点或者组件都会生成一个与之对应的fiber节点(生成的过程会在后续的文章中进行解读)
// 在后续的调和(reconciliation)阶段起着至关重要的作用
const uninitializedFiber = createHostRootFiber(tag);
// 创建完rootFiber之后,会将fiberRoot实例的current属性指向刚创建的rootFiber
root.current = uninitializedFiber;
// 同时rootFiber的stateNode属性会指向fiberRoot实例,形成相互引用
uninitializedFiber.stateNode = root;
// 最后将创建的fiberRoot实例返回
return root;
}
一个完整的FiberRootNode
实例包含了很多有用的属性,这些属性在任务调度阶段都发挥着各自的作用,可以在ReactFiberRoot.js
文件中看到完整的FiberRootNode
构造函数的实现(这里只列举部分属性):
/**
-
FiberRootNode构造函数
-
@param containerInfo DOM容器
-
@param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
-
@param hydrate 判断是否是hydrate模式
-
@constructor
*/
function FiberRootNode(containerInfo, tag, hydrate) {
// 用于标记fiberRoot的类型
this.tag = tag;
// 指向当前激活的与之对应的rootFiber节点
this.current = null;
// 和fiberRoot关联的DOM容器的相关信息
this.containerInfo = containerInfo;
…
// 当前的fiberRoot是否处于hydrate模式
this.hydrate = hydrate;
…
// 每个fiberRoot实例上都只会维护一个任务,该任务保存在callbackNode属性中
this.callbackNode = null;
// 当前任务的优先级
this.callbackPriority = NoPriority;
…
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档,需要的读者可以戳这里免费领取!
ty;
…
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-fWPZygDN-1713552740618)]
[外链图片转存中…(img-BGp78eT4-1713552740619)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-bRtDxoex-1713552740619)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
[外链图片转存中…(img-QQlVO44L-1713552740620)]
最后
前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档,需要的读者可以戳这里免费领取!
[外链图片转存中…(img-9LoZDXPc-1713552740620)]
[外链图片转存中…(img-JGtKgGrZ-1713552740620)]