前言
本篇是React源码解析系列第三篇。主要学习React的初始化和初次渲染。源码版本为v18.2.0。
阅读本篇需要Fiber基础,可以查看# React源码解析(二):Fiber架构。
具体源码解析会略过诸如React校验、优先级调度、事件处理机制等复杂内容,也就是说本篇讲解的源码是一个简化版本,方便阅读理解。可以查看React源码进行对比。
初始化流程
从使用角度来说,我们初始化一个React App需要以下两个步骤:
import ReactDOM from "react-dom/client";
import App from "./App";
// 第一步 创建root类的实例
const root = ReactDOM.createRoot(document.getElementById("root"));
// 第二步 初次渲染
root.render(<App />);
第一步,我们先来了解 ReactDOM.createRoot 做了什么。
创建根
React的根是比较特殊的,因为创建时我们会传入指定的根容器结点,也就是说一开始根节点就是有真实DOM的,由此引入React对根的两个定义:FiberRootNode 和 HostRootFiber。
FiberRootNode
: 指的是root节点,它包含了root的真实DOM(containerInfo)。HostRootFiber
: 指的是root节点对应的Fiber。 它们之间的关系如图所示,各有一个指针指向对方。FiberRootNode的containerInfo属性即为传入的根容器的真实DOM。 了解了这个我们来看下React创建根具体做了什么。
createRoot
// react-dom/src/client/ReactDOMRoot.js
import {createContainer} from "react-reconciler/src/ReactFiberReconciler";
// root类
function ReactDOMRoot(internalRoot){
this._internalRoot = internalRoot;
}
// container即为传入的真实DOM div#root
export function createRoot(container){
// 创建FiberRootNode
const root = createContainer(container);
return new ReactDOMRoot(root);
}
简化后可以发现createRoot做的事情很简单,创建了一个root对象(FiberRootNode),并将其挂在实例化的ReactDOMRoot上,方便后面使用,重点来看root所代表的FiberRootNode是如何创建的。
// react-reconciler/src/ReactFiberReconciler.js
import { createFiberRoot } from './ReactFiberRoot';
export function createContainer(containerInfo){
return createFiberRoot(containerInfo);
}
// react-reconciler/src/ReactFiberRoot.js
import { createHostRootFiber } from "./ReactFiber";
import { initialUpdateQueue } from "./ReactFiberClassUpdateQueue";
function FiberRootNode(containerInfo){
// containerInfo即为root的真实DOM div#root
this.containerInfo = containerInfo;
}
export function createFiberRoot(containerInfo){
const root = new FiberRootNode(containerInfo);
// uninitializedFiber即为HostRootFiber
const uninitializedFiber = createHostRootFiber();
// FiberRootNode和HostRootFiber互相指向
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 初始化更新队列
initialUpdateQueue(uninitializedFiber);
return root;
}
ReactFiber.js是为了专门去创建Fiber的文件
// react-reconciler/src/ReactFiber.js
import { HostRoot } from "./ReactWorkTags";
export function createHostRootFiber() {
return createFiber(HostRoot, null, null);
}
// 创建Fiber实例
export function createFiber(tag, pendingProps, key) {
return new FiberNode(tag, pendingProps, key);
}
/**
* Fiber类
* @param {*} tag Fiber的类型tag,如函数组件、原生组件、类组件等等虚拟dom对应编号
* @param {*} pendingProps 新属性,等待处理或生效的属性
* @param {*} key 唯一标识
*/
export function FiberNode(tag, pendingProps, key) {
// 虚拟dom对应的标识
this.tag = tag;
this.key = key;
// 虚拟dom的类型,如div、span、p 函数组件和类组件则是本身函数或类
this.type = null;
// 此Fiber对应的真实dom节点
this.stateNode = null;
// 父节点
this.return = null;
// 第一个子节点
this.child = null;
// 第一个弟弟节点
this.sibling = null;
// 等待生效的属性
this.pendingProps = pendingProps;
// 已经生效的属性
this.memoizedProps = null;
// 每种React元素存的类型是不一样的,如类组件对应的fiber存的就是实例的状态,HostRoot存的就是要渲染的元素
this.memoizedState = null;
// 更新队列
this.updateQueue = null;
// 替身 双缓冲机制 dom-diff的时候用
this.alternate = null;
this.ref = null;
// 省略一些未涉及的属性
...
}
ReactWorkTags.js是存放React虚拟DOM的tag标识
// react-reconciler/src/ReactWorkTags.js
// 函数组件
export const FunctionComponent = 0;
// 类组件
export const ClassComponent = 1;
// 未定组件 因为函数组件和类组件都是一个函数
export const IndeterminateComponent = 2;
// 容器根节点
export const HostRoot = 3;
// 原生节点
export const HostComponent = 5;
// 文本节点
export const HostText = 6;
// react-reconciler/src/ReactFiberClassUpdateQueue.js
// 初始化fiber的更新队列
export function initialUpdateQueue(fiber) {
const queue = {
shared: {
pending: null,
},
};
fiber.updateQueue = queue;
}
至此我们完成了创建root的流程,完成后真实DOM、ReactDOMRoot、FiberRootNode、HostRootFiber的关系如下:
初次渲染
初次渲染会调用 ReactDOMRoot的 render 方法。首先先添上这个方法。
// react-dom/src/client/ReactDOMRoot.js
import {
createContainer,
updateContainer,
} from "react-reconciler/src/ReactFiberReconciler";
ReactDOMRoot.prototype.render = function(children){
// 取出FiberRootNode
const root = this._internalRoot;
// 初次渲染
updateContainer(children, root);
}
// react-reconciler/src/ReactFiberReconciler.js
import { createUpdate, enqueueUpdate } from "./ReactFiberClassUpdateQueue";
import { scheduleUpdateOnFiber } from "./ReactFiberWorkLoop";
export function updateContainer(element, container) {
// 获取HostRootFiber
const current = container.current;
// 创建更新对象
const update = createUpdate();
// 要更新的虚拟dom
update.payload = { element };
// 入队更新 把此更新对象添加到HostRootFiber的更新队列上
const root = enqueueUpdate(current, update);
// 对Fiber进行调度更新
scheduleUpdateOnFiber(root, current);
}
// react-reconciler/src/ReactFiberClassUpdateQueue.js
export const UpdateState = 0;
export function createUpdate() {
const update = {
tag: UpdateState,
next: null,
};
return update;
}
/*
源码中,此处会入队并发队列,我们在这边进行简化处理,只需要知道
这个方法会将新的更新对象串成一个循环链表,并返回FiberRootNode即可
*/
export function enqueueUpdate(fiber, update) {
const updateQueue = fiber.updateQueue;
const pending = updateQueue.shared.pending;
// 将update串成一个循环链表
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
updateQueue.shared.pending = update;
/*
向上找到FiberRootNode
*/
let parent = sourceFiber.return;
// FiberRootNode的parent为null
while (parent !== null) {
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
// FiberRootNode
return node.stateNode;
}
return null;
}
至此,我们创建了更新对象并将其放到了Fiber的updateQueue上只剩下最后的调度更新
Fiber上的更新循环链表
此处穿插讲解一下Fiber上的更新是如何存储的,还记得我们初始化Fiber的updateQueue吗?它的shared属性有一个pending指向等待生效的更新,初始时pending为null。
当有更新入队时,pending便会指向这次更新,同时,这些更新会串成一个 循环链表,也就是说,pending指向的是 最后一次更新,最后一次更新会指向第一次更新,第一次更新会指向第二次更新,第二次更新指向第三次更新如此循环。用图表示会更清晰一些。
第一个更新入队
第二个更新入队
第三个更新入队
可以思考一下为什么要采用循环链表的形式。
因为循环链表的形式操作简便且性能高效,为什么不采用数组呢?因为为了高优先级打断低优先级的操作,更新不是一定按照顺序的,链表的拼接会更加简单高效。为什么不采用单向链表呢?可以思考一下有更新加入单链表是不是会更加复杂?需要循环到next为null的节点再插入。
scheduleUpdateOnFiber
接下来便进入我们本篇的重点,workLoop,再次强调一下,本篇源码讲解略过了如优先级调度、hooks等复杂内容,以先了解react大概架构为主,后续文章会再补充进行解析。
默认情况下,react的渲染都是并行的(Concurrent),为了简便,第一次渲染我们先认为是同步的。
// react-reconciler/src/ReactFiberWorkLoop.js
// 当前工作中的Fiber树 即替身Fiber树
let workInProgress = null;
export function scheduleUpdateOnFiber(root) {
ensureRootIsScheduled(root);
}
function ensureRootIsScheduled(root) {
/*
此处Scheduler_scheduleCallback会去向浏览器每帧拿5ms时间执行performSyncWorkOnRoot
本篇先略过,后续讲解
*/
Scheduler_scheduleCallback(performSyncWorkOnRoot.bind(null, root));
}
// 执行同步任务
function performSyncWorkOnRoot(root) {
// 同步render
renderRootSync(root);
// 同步render完成后 root.current.alternate即为新的fiber树
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
// 开始进入提交阶段,就是执行副作用修改真实dom 下一篇讲解
commitRoot(root);
}
function renderRootSync(root) {
// 准备一棵新的Fiber树
prepareFreshStack(root);
// 同步工作循环
workLoopSync();
}
function prepareFreshStack(root) {
// root.current即为HostRootFiber
workInProgress = createWorkInProgress(root.current, null);
}
// 同步工作循环类似Stack reconciler,不会中断,执行到清空工作单元为止
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
// 获取新fiber对应的老fiber
const current = unitOfWork.alternate;
// 当前fiber的子fiber链表构建 dom-diff 本篇略过 后续讲解
const next = beginWork(current, unitOfWork);
// beginWork会将pendingProps更新 此处将当前待生效的属性标记为已生效的属性
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 没有子节点代表当前的fiber已经完成了
if (next === null) {
// 完成工作单元
completeUnitOfWork(unitOfWork);
} else {
// 有子节点则成为下一个工作单元
workInProgress = next;
}
}
// 完成一个工作单元
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
// 最后的HostRootFiber的return 为null
const returnFiber = completedWork.return;
// 执行此fiber的完成工作 如果是原生组件的话需要创建真实dom节点 本篇先略过 后续讲解
completeWork(current, completedWork);
// 执行当前fiber的弟弟节点
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
// 如果没有弟弟,说明当前fiber是父fiber的最后一个节点
// 再次进入while循环 完成父fiber
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
// react-reconciler/src/ReactFiber.js
/*
根据当前Fiber树创建新的Fiber树
*/
export function createWorkInProgress(current, pendingProps){
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(current.tag, pendingProps, current.key);
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
}
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
return workInProgress;
}
这么一长串代码看下来肯定有些晕头晕脑,我们来梳理一下。
- 调度开始,每帧向浏览器请求5ms时间调度任务,如何请求先略过,后面讲解。
- 执行renderRootSync,根据当前的Fiber树创建一棵新的树,如果有替身树则复用,称为workInProgress,执行工作循环workLoopSync。
- 循环每个工作单元,还记得上一篇文章的Fiber树的执行顺序吗?是的,深度遍历,先遍历节点的第一个孩子,如果当前节点没有孩子则完成当前节点,寻找当前节点的弟弟,如果也没有弟弟则返回父节点,完成父节点,寻找父节点的弟弟,直到根节点。其中beginWork和completeWork我们留到下一篇讲解。
- 进入提交阶段 commitRoot,去处理节点的副作用,如插入、删除等。
至此,我们大概了解了React初次渲染的步骤,这些步骤后续更新时也会进行复用。
本文正在参加「金石计划」