哈喽大家好,好久没有跟大家以技术文章的形式见面了。最近本人在使用 React 18 做 Web 项目,所以抽空研究了一下 React 18 的源代码。接下来想做一个 React 18 源码分析的系列,系列文章会以「 demo + 源码 」的形式由浅入深地跟大家一起探讨新版本的 React 的技术实现,欢迎点赞 / 关注 / 拍砖,一起进步
本系列将默认使用 React v18.1.0 版本,默认运行环境为浏览器,读者可自行到 GitHub 下载 React 源码,敬请留意
本章我们将探讨 React 项目初始化的时候做了哪些事情:
Demo
我们使用 create-react-app 这个官方脚手架创建一个 React 项目,然后将 index.js
这个文件修改为以下代码
import { createRoot } from 'react-dom/client';
function App() {
return Hello dan!!!
}
const root = createRoot(document.getElementById('root'))
root.render( )
执行 npm start
这个脚本,如果你看到这个非常简单(丑陋)的页面显示出来,那证明项目已经可以正常运行起来了
函数分析
从 Demo 我们看到,整个项目先通过 createRoot
这个函数创建一个 root
对象,再通过 root
的 render
方法将 App
这个组件渲染到网页上
createRoot
我们先看 createRoot
这个方法具体做了什么事情。这个方法来自 react-dom
这个包。我们可以在源码中 packages / react-dom / src / client / ReactDOMRoot.js
中找到 createRoot
的具体实现(前面在 ReactDOM.js 做了一些关于环境的条件判断,可先忽略)
createRoot
函数有两个参数 container
和 options
,其中 options
是可选参数,本章为了简单起见先不讨论;
该函数大概实现的功能就是:
- 创建容器对象
FiberRootNode
- 事件委托处理
- 根据
FiberRootNode
对象返回ReactDOMRoot
对象
// 删除了一些干扰逻辑之后,createRoot 函数大致如下所示
function createRoot(
container: Element | DocumentFragment,
options?: CreateRootOptions,
): RootType {
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
// 创建容器对象 `FiberRootNode`
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
// 事件监听处理
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
// 根据容器对象 `root` 返回 `ReactDOMRoot` 对象
return new ReactDOMRoot(root);
}
createContainer
函数完成了 创建容器对象 FiberRootNode
的工作。
这个方法来自 react-reconciler
这个包,可以在 packages / react-reconciler / src / ReactFiberReconciler.old.js
中找到,而这个方法内容也很简单,直接调用了同层级 ReactFiberRoot.old.js
文件的 createFiberRoot
方法来创建并返回一个 FiberRootNode
对象,也称之为 Fiber 根结点
。
注意: 这里的 Fiber 根结点
跟 Fiber 节点
是有区别的,详细可看下面的各自的定义函数
这里引入一个概念叫做 Fiber,目前我们只需要对他有个初步的印象:Fiber 节点用于存储 React 组件节点信息(包括 DOM节点,组件的属性 / state / effect 等)。这里可以简单理解为一个存储信息的 JS 对象,后续章节会详细介绍
function createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride,
identifierPrefix, onRecoverableError, transitionCallbacks) {
// 创建 FiberRootNode 对象
// tag 值为 ConcurrentRoot,定义在 packages/react-reconciler/src/ReactRootTags.js 文件中;
// tag === ConcurrentRoot === 1 ,表示 “根节点”
var root = new FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError);
// 建议看到这里先别着急看后面的代码,先看看下面 FiberRootNode 的定义和构造函数
// 分割线 ************** 分割线 ************** 分割线 ************** 分割线 **************
// 看完 FiberRootNode 的定义之后,接下来马上要创建 Fiber 对象
// createHostRootFiber 会调用 packages/react-reconciler/src/ReactFiber.old.js 文件中的 createFiber 方法创建一个 `Fiber HostRoot节点`
// `Fiber HostRoot节点` 就是一个 Fiber 对象,只是他的 Tag 等于 3,代表 `HostRoot`
var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
// 把 `Fiber 根结点` 的 current 属性指向刚创建的 `Fiber HostRoot节点`
root.current = uninitializedFiber;
// `Fiber HostRoot节点` 的 stateNode 属性指向 `Fiber 根结点`
uninitializedFiber.stateNode = root;
// cache 相关的可先忽略
var initialCache = createCache();
retainCache(initialCache);
root.pooledCache = initialCache;
retainCache(initialCache);
// 初始化一个 state 对象
var initialState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
transitions: null
};
uninitializedFiber.memoizedState = initialState;
// 初始化 `Fiber HostRoot节点` 的更新队列
// 给 Fiber 的 updateQueue 属性赋值
/**
var queue = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes
},
effects: null
};
fiber.updateQueue = queue;
**/
initializeUpdateQueue(uninitializedFiber);
// 返回 `Fiber 根结点`
return root;
}
FiberRootNode
的定义:
一个构造函数,对象内保存Fiber 根节点
的信息,可先关注以下几个
- tag:标识节点类型,此处为
ConcurrentRoot
- containerInfo:
Fiber 根节点
的 DOM 信息,表示在这个 DOM 节点内部渲染当前 React 应用 - current:保存当前 Fiber 树(后续章节会讲到)
其他属性可以先大致扫一遍,重要是的后续会逐个介绍
// 在`packages/react-reconciler/src/ReactFiberRoot.old.js`文件中 // FiberRootNode 构造函数 function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError) { this.tag = tag; this.containerInfo = containerInfo; this.pendingChildren = null; this.current = null; .... // 省略其他属性初始化 ....
Fiber 节点的定义
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// tag 表示 Fiber 类型
// packages/react-reconciler/src/ReactWorkTags.js 中定义
this.tag = tag;
// 写在 jsx 组件上的 key 属性
this.key = key;
// createElement的第一个参数,ReactElement 上的 type
this.elementType = null;
// 暂时可认为与 elementType 基本一致
this.type = null;
// fiber 节点对应的 DOM 节点
this.stateNode = null;
// Fiber 结构
// 指向父节点
this.return = null;
// 指向第一个子节点
this.child = null;
// 指向兄弟节点
this.sibling = null;
// 一般如果没有兄弟节点的话是 0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index
this.index = 0;
// 保存 ref 属性对象
this.ref = null;
// 新的 props 对象
this.pendingProps = pendingProps;
// 现有的 props 对象
this.memoizedProps = null;
// 保存更新对象的队列
this.updateQueue = null;
// 现有的 state 对象
this.memoizedState = null;
// 依赖对象
this.dependencies = null;
// 渲染方式
// React 18 默认是 `ConcurrentMode`: 0b000001
// packages/react-reconciler/src/ReactTypeOfMode.js 文件中定义
this.mode = mode;
// Effects
// effect 的 Flag,表明当前的 effect 是`替换`/ `更新` / `删除` 等操作
// packages/react-reconciler/src/ReactFiberFlags.js
this.flags = NoFlags;
// 子树的 Flag 合集
this.subtreeFlags = NoFlags;
// 需要删除的 fiber 节点
this.deletions = null;
// 更新渲染调度优先级相关
// packages/react-reconciler/src/ReactFiberLane.old.js 文件中定义
this.lanes = NoLanes;
this.childLanes = NoLanes;
// current 树和 workInprogress 树之间的相互引用
// current 树就是当前的 Fiber 树
// workInprogress 树 就是正在更新的 Fiber 树
// 后续讲到组件更新会详细讲到
this.alternate = null;
if (enableProfilerTimer) {
// 。。。 省略
}
}
总结一下: createContainer
方法通过 createFiberRoot
创建并返回 Fiber 根节点
:FiberRootNode
对象。同时该对象的 current 属性指向一个 Fiber HostRoot节点
。
markContainerAsRoot
方法在容器 DOM 节点上添加一个属性 __reactContainer${randomKey}
,属性的值指向Fiber HostRoot节点
。以表明该 DOM 节点为当前 React 应用的容器节点。
listenToAllSupportedEvents
函数完成了 事件委托处理 的工作
在 packages/react-dom/src/events/DOMPluginEventSystem.js
文件中,listenToAllSupportedEvents
函数接收一个入参:容器 DOM 节点(也就是createRoot
函数的第一个参数)
大致的原理是:React 18 把所有事件都委托到这个节点上面,一旦原生事件触发之后,这个节点会根据事件类型以及优先级,触发对应 fiber 节点上的事件回调函数。目前可以先了解一下 React 合成事件,后续章节讲到事件机制会详细讲解
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
// allNativeEvents 是一个集合,保存了 React 支持的所有事件
// Set(81) {'abort', 'auxclick', 'cancel', 'canplay', 'canplaythrough', …}
allNativeEvents.forEach(domEventName => {
// 这里最重要的函数就是 `listenToNativeEvent`
// 用于将事件绑定到容器的 DOM 节点
// 下面会根据是否响应捕获阶段分逻辑处理(可先忽略)
// selectionchange 事件也单独处理(可先忽略)
// listenToNativeEvent 事件内部调用 addTrappedEventListener 函数
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
// 。。。省略 selectionchange 逻辑
}
}
addTrappedEventListener
函数主要实现:根据事件获取对应的优先级,不同的优先级在容器 DOM节点
注册不同的事件回调函数
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
// packages/react-reconciler/src/ReactEventPriorities.js 文件保存事件优先级的定义
// createEventListenerWrapperWithPriority逻辑 :
// 1. 调用`getEventPriority` 函数实现从`事件名`到 `事件优先级` 的转化
// 2. 根据 `事件优先级` eventPriority 匹配不同的回调函数:(dispatchDiscreteEvent,dispatchContinuousEvent, dispatchEvent)
// 3. 返回事件回调函数,赋值给 listener
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); // If passive option is not supported, then the event will be
var isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
// 逻辑省略
}
targetContainer = targetContainer;
var unsubscribeListener;
// 事件绑定逻辑:
// 调用 addEventCaptureListener(WithPassiveFlag) / addEventBubbleListener((WithPassiveFlag)) 函数进行事件绑定,
// 内部调用原生方法 dom.addEventListener,实现事件绑定
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
}
}
}
总结一下:listenToAllSupportedEvents
函数根据不同的事件类型,给容器 DOM节点
注册不同的回调函数,子组件的所有事件都由该节点进行分发和触发
返回 ReactDOMRoot 对象
实例化 ReactDOMRoot
对象,将 Fiber HostRoot节点
传人构造函数中,保存在对象的 _internalRoot
属性
// ReactDOMRoot 构造函数
// 比较简单,不解释
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
createRoot
函数最后返回 ReactDOMRoot
对象,完成整个函数的所有工作。接下来,调用ReactDOMRoot
对象的 render
方法进行渲染工作
render
render
方法在 packages/react-dom/src/client/ReactDOMRoot.js
文件中实现,入参是子组件,函数内部调用了 updateContainer
方法对子组件(App)进行渲染
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
const container = root.containerInfo;
// 重要步骤,重点分析
updateContainer(children, root, null, null);
};
updateContainer
函数在 packages/react-reconciler/src/ReactFiberReconciler.old.js
文件中定义,主要实现容器的调度任务
Lane 在 React 中用于表示任务的优先级,目前只需要有个大概的了解,后续会详细讲解
schedule 是一个独立的任务调度模块,目前只用于 React 内部,很多 API 还处于 unstable 状态,后续有可能会提供给外部项目使用;这个模块也会在后续单独讲解,敬请期待
// 删除一些干扰逻辑之后的 `updateContainer` 函数
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component,
callback: ?Function,
): Lane {
// 当前的 Fiber 树
const current = container.current;
// 当前事件时间,调用 `now` 函数
const eventTime = requestEventTime();
// 获取当前更新的 lane (任务调度优先级)
const lane = requestUpdateLane(current);
// 获取上下文
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
// 将 FiberRootNode 的 context 属性指向 context
container.context = context;
} else {
// 将 FiberRootNode 新的 context 属性指向 context
container.pendingContext = context;
}
// 创建一个`更新对象`:update
/* var update = {
eventTime: eventTime, // 事件时间
lane: lane, // 调度优先级
tag: UpdateState, // 标识是 update / delete / ForceUpdate / ...
payload: null, // payload,保存 { element: React.element }
callback: null, // 回调函数
next: null // 指向下一个 update
};
*/
const update = createUpdate(eventTime, lane);
// 设置更新对象的 paylaod 属性
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 把`更新对象` enqueue 到`更新队列`
// 后续在将组件更新的时候会细讲,这块还是比较重要的,目前可以大概了解
enqueueUpdate(current, update, lane);
// scheduleUpdateOnFiber 利用到 scheduler 这个包来进行任务调度
// 通过将渲染方法 performConcurrentWorkOnRoot 注册到 scheduler 的调度机制中
// scheduler 会根据任务优先级执行这个渲染方法,将 APP 组件最终渲染到页面上
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}
return lane;
}
流程图
至此,整个 React 项目的初始化过程就完成了,为了保证本章内容足够简单,很多细节都还没有深入讲解。不过,相信读者读完本章后,对整个初始化的过程也有了一定的了解。在后续的章节中,我们将针对 React 项目的其他阶段进行深入剖析。同时也欢迎各位读者可以在下方给我留言一起交流,共同进步
最后最后,希望疫情能够早日结束,中国加油,世界加油 !!!