总结
大厂面试问深度,小厂面试问广度,如果有同学想进大厂深造一定要有一个方向精通的惊艳到面试官,还要平时遇到问题后思考一下问题的本质,找方法解决是一个方面,看到问题本质是另一个方面。还有大家一定要有目标,我在很久之前就想着以后一定要去大厂,然后默默努力,每天看一些大佬们的文章,总是觉得只有再学深入一点才有机会,所以才有恒心一直学下去。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
): _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;
…
}
部分属性信息如上所示,由于属性过多并且在本文中暂时还用不到,这里就先不一一列举出来了,剩余的属性及其注释信息已经上传至Github(https://github.com/qq591468061/react-16.10.2),感兴趣的朋友可以自行查看。在了解完了fiberRoot
的属性结构之后,接下来继续探究createFiberRoot
方法的后半部分内容:
// 以下代码来自上文中的createFiberRoot方法
// 通过createHostRootFiber方法创建fiber tree的根节点,即rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 创建完rootFiber之后,会将fiberRoot实例的current属性指向刚创建的rootFiber
root.current = uninitializedFiber;
// 同时rootFiber的stateNode属性会指向fiberRoot实例,形成相互引用
uninitializedFiber.stateNode = root;
// 以下代码来自ReactFiber.js文件
/**
-
内部调用createFiber方法创建一个FiberNode实例
-
@param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
-
@returns {Fiber}
*/
export function createHostRootFiber(tag: RootTag): Fiber {
let mode;
// 以下代码根据fiberRoot的标记类型来动态设置rootFiber的mode属性
// export const NoMode = 0b0000; => 0
// export const StrictMode = 0b0001; => 1
// export const BatchedMode = 0b0010; => 2
// export const ConcurrentMode = 0b0100; => 4
// export const ProfileMode = 0b1000; => 8
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BatchedMode | StrictMode;
} else if (tag === BatchedRoot) {
mode = BatchedMode | StrictMode;
} else {
mode = NoMode;
}
…
// 调用createFiber方法创建并返回一个FiberNode实例
// HostRoot表示fiber tree的根节点
// 其他标记类型可以在shared/ReactWorkTags.js文件中找到
return createFiber(HostRoot, null, null, mode);
}
/**
-
创建并返回一个FiberNode实例
-
@param tag 用于标记fiber节点的类型(所有的类型存放在shared/ReactWorkTags.js文件中)
-
@param pendingProps 表示待处理的props数据
-
@param key 用于唯一标识一个fiber节点(特别在一些列表数据结构中,一般会要求为每个DOM节点或组件加上额外的key属性,在后续的调和阶段会派上用场)
-
@param mode 表示fiber节点的模式
-
@returns {FiberNode}
*/
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn’t like constructors
// FiberNode构造函数用于创建一个FiberNode实例,即一个fiber节点
return new FiberNode(tag, pendingProps, key, mode);
};
至此我们就成功地创建了一个fiber
节点,上文中我们提到过,和 DOM 树结构类似,fiber
节点也会形成一个与 DOM 树结构对应的fiber tree
,并且是基于单链表的树结构,我们在上面刚创建的fiber
节点可作为整个fiber tree
的根节点,即RootFiber
节点。在目前阶段,我们暂时不用关心一个fiber
节点所包含的所有属性,但可以稍微留意一下以下相关属性:
/**
-
FiberNode构造函数
-
@param tag 用于标记fiber节点的类型
-
@param pendingProps 表示待处理的props数据
-
@param key 用于唯一标识一个fiber节点(特别在一些列表数据结构中,一般会要求为每个DOM节点或组件加上额外的key属性,在后续的调和阶段会派上用场)
-
@param mode 表示fiber节点的模式
-
@constructor
*/
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
// 用于标记fiber节点的类型
this.tag = tag;
// 用于唯一标识一个fiber节点
this.key = key;
…
// 对于rootFiber节点而言,stateNode属性指向对应的fiberRoot节点
// 对于child fiber节点而言,stateNode属性指向对应的组件实例
this.stateNode = null;
// Fiber
// 以下属性创建单链表树结构
// return属性始终指向父节点
// child属性始终指向第一个子节点
// sibling属性始终指向第一个兄弟节点
this.return = null;
this.child = null;
this.sibling = null;
// index属性表示当前fiber节点的索引
this.index = 0;
…
// 表示待处理的props数据
this.pendingProps = pendingProps;
// 表示之前已经存储的props数据
this.memoizedProps = null;
// 表示更新队列
// 例如在常见的setState操作中
// 其实会先将需要更新的数据存放到这里的updateQueue队列中用于后续调度
this.updateQueue = null;
// 表示之前已经存储的state数据
this.memoizedState = null;
…
// 表示fiber节点的模式
this.mode = mode;
// 表示当前更新任务的过期时间,即在该时间之后更新任务将会被完成
this.expirationTime = NoWork;
// 表示当前fiber节点的子fiber节点中具有最高优先级的任务的过期时间
// 该属性的值会根据子fiber节点中的任务优先级进行动态调整
this.childExpirationTime = NoWork;
// 用于指向另一个fiber节点
// 这两个fiber节点使用alternate属性相互引用,形成双缓冲
// alternate属性指向的fiber节点在任务调度中又称为workInProgress节点
this.alternate = null;
…
}
其他有用的属性笔者已经在源码中写好相关注释,感兴趣的朋友可以在Github上查看完整的注释信息帮助理解。当然在现阶段,其中的一些属性还暂时难以理解,不过没有关系,在后续的内容和系列文章中将会逐个击破。在本小节中我们主要是为了理解FiberRoot
和RootFiber
这两个容易混淆的概念以及两者之间的联系。同时在这里我们需要特别注意的是,多个fiber
节点可形成基于单链表的树形结构,通过自身的return
,child
和sibling
属性可以在多个fiber
节点之间建立联系。为了更加容易理解多个fiber
节点及其属性之间的关系,这里先回顾一下在上一篇文章中的简单示例,我们在src/App.js
文件中将create-react-app
脚手架生成的默认根组件App
修改为如下形式:
import React, {Component} from ‘react’;
最后
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
最后写上我自己一直喜欢的一句名言:
世界上只有一种真正的英雄主义就是在认清生活真相之后仍然热爱它