作为一名前端小白,会想起第一次debug React组件时的手足无措,在惊叹这个框架的奇妙之余也更加觊觎其中的道理。
万物伊始——react准备工作
每个react的web应用想要展示到页面上的第一步都需要执行渲染函数ReactDOM.createRoot(document.getElementById('root')).render(<App />)
,而它都做了些什么呢?
首先我们看到的是ReactDOM
调用的createRoot
方法,它在react内部调用createRoot方法返回一个React容器对象
export function createRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMRoot(container, options);
}
参数中的container
,也就是我们通过document.getElementById('root')
获取的dom对象,options
这个参数,它主要用在服务端渲染流程中,所以现在为空。
再看,这个函数中返回的是ReactDOMRoot构造函数生成的对象,而ReactDOMRoot
又是干什么呢?
function ReactDOMRoot(container: Container, options: void | RootOptions) {
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
它为我们创建的React容器对象的_internalRoot
属性通过createRootImpl
函数的返回值赋值,他又是做什么的呢。
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
...
// 创建FiberRoot对象
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
// 标记一下FiberRoot对象
markContainerAsRoot(root.current, container);
// 获取整个应用的挂在的那个元素
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
// 注册事件代理
listenToAllSupportedEvents(rootContainerElement);
...
return root;
}
首先我们看到这个函数的参数由之前的两个变成了三个,中间的tag参数是用来标志react的工作模式。自从React16之后, react引入了Fiber,工作模式也由之前的一种变成两种,随之改变的是在react应用挂载时执行的渲染函数也由一种变成了三种。
ReactDOM.render(<App />, document.getElementById('root'))
ReactDOM.createBlockingRoot(document.getElementById('root')).render(<App />)
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
第一种还是原始默认,其工作方式也是同步方式,而剩下两种则是使用新版的并行工作方式。
而它们之间就是通过刚才提到的tag参数进行区分的,而createBlockingRoot
和createRoot
之间借用官方的解释:
export type RootTag = 0 | 1 | 2;
export const LegacyRoot = 0;
export const BlockingRoot = 1;
export const ConcurrentRoot = 2;
进入这个函数,最主要的就是做了两件事
- 生成
FiberRoot
对象。 - 注册事件代理。
我们先看第一步,通过调用createContainer
函数生成root
并最终返回这个root
,这也就是_internalRoot
的值。
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
// 创建并返回FiberRoot
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
createContainer
中又调用了createFiberRoot
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 实例化一个FiberRoot对象
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
...
// 创建一个当前组件树的根节点HostRootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 将这个HostRootFiber对象作为FiberRoot对象current属性的引用
root.current = uninitializedFiber;
// 这个FiberRoot对象的stateNode属性指向容器对象
uninitializedFiber.stateNode = root;
// 初始化更新队列
initializeUpdateQueue(uninitializedFiber);
return root;
}
首先,调用FiberRootNode
函数实例化root
对象,这个函数会返回一个RootNode
对象,赋值给root并最终也返回它。
而之后调用createHostRootFiber
创建HostRootFiber
对象,赋值给uninitializedFiber
这里的createHostRootFiber
函数最终返回的是一个Fiber
对象。而传给createHostRootFiber
的参数是当前工作模式的标志。
此时将uninitializedFiber
赋值到root.current
属性上,作为当前的组件树的根节点。
接下来初始化更新队列,调用initializeUpdateQueue
函数
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
// 为fiber节点也就是当前组件树的根节点的updateQueue属性赋初始值
fiber.updateQueue = queue;
}
最后返回root
,返回并结束我们在createRootImpl
中的第一步生成FiberRoot
对象。
接下来执行listenToAllSupportedEvents
,参数为依然是通过document.getElementById('root')
获取的dom对象
const listeningMarker =
'_reactListening' +
Math.random()
.toString(36)
.slice(2);
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
// 当(rootContainerElement: any)[listeningMarker]不为true时向下执行
if ((rootContainerElement: any)[listeningMarker]) {
// 优化:在相同的portal容器或者根节点只迭代一次
// 一旦移除这个标记,我们也许还可以删除一些用于懒加载的簿记图。
return;
}
// 设置(rootContainerElement: any)[listeningMarker]为true,确保不再重复执行。
(rootContainerElement: any)[listeningMarker] = true;
// 为所有的原生事件添加监听
allNativeEvents.forEach(domEventName => {
// 判断当前原生事件是否在不需要代理监听的事件列表里,如果不在则执行
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(
domEventName,
false,
((rootContainerElement: any): Element),
null,
);
}
listenToNativeEvent(
domEventName,
true,
((rootContainerElement: any): Element),
null,
);
});
}
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
// selectionchange需要附加到文档上,否则它将无法捕获仅直接在文档上触发的传入事件。
if (
domEventName === 'selectionchange' &&
(rootContainerElement: any).nodeType !== DOCUMENT_NODE
) {
target = (rootContainerElement: any).ownerDocument;
}
// If the event can be delegated (or is capture phase), we can
// register it to the root container. Otherwise, we should
// register the event to the target element and mark it as
// a non-delegated event.
if (
targetElement !== null &&
!isCapturePhaseListener &&
nonDelegatedEvents.has(domEventName)
) {
// 对于所有未委派的事件,除了滚动之外,还将其事件侦听器附加到其事件触发的各个元
// 素上。 这意味着我们可以跳过此步骤,因为以前已经添加了事件侦听器。 但是,我们对滚
// 动事件进行了特殊处理,因为现实是任何元素都可以滚动。
if (domEventName !== 'scroll') {
return;
}
eventSystemFlags |= IS_NON_DELEGATED;
target = targetElement;
}
// 创建或者获取事件代理集合
const listenerSet = getEventListenerSet(target);
// 创建代理事件名称并标记这个事件是捕获还是冒泡
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
// 如果是新增事件或者更新事件
if (!listenerSet.has(listenerSetKey)) {
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
// 添加绑定事件
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
// 将新增或更新事件添加到事件集合中
listenerSet.add(listenerSetKey);
}
}
至此,我们完成了万里长征的第一步,react在渲染更新前的准备工作
- 初始化根节点
- 初始化事件代理
万事开头难,然后中间难,最后会更难。每天挪一小步,光就会出现。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!