前言
相信写过React项目的朋友都不陌生,React渲染DOM的过程是通过项目入口文件index.js中调用了ReactDOM.render方法,将JSX代表的虚拟DOM渲染在了项目的DOM根节点上。我们知道虚拟DOM是为了通过JS对象的方式来表示复杂的真实DOM节点,而在React的渲染更新过程中,又引入了Fiber的数据结构,便于协调过程中进行diff算法,从而以最小化的操作更新DOM,来达到优化性能的目的。下面会实现一个从JSX到创建简单的Fiber数据结构,再到构建真实DOM渲染到页面的过程。
前置准备
以下是一个测试页面,JSX中包含了原生节点,函数组件,类组件以及Fragment节点,便于对实现的功能进行测试。
// index.js
import React, { Component } from "react";
import ReactDOM from "react-dom";
import "./index.css";
class ClassComponent extends Component {
render() {
return (
<div className="border">
<p>{this.props.title}</p>
</div>
);
}
}
function FunctionComponent(props) {
return (
<div className="border">
<p>{props.title}</p>
</div>
);
}
const jsx = (
<div className="border">
<h3>Learn React</h3>
<a href="https://reactjs.org/">react doc</a>
<FunctionComponent title="FcnCmp" />
<ClassComponent title="ClsCmp" />
<ul>
<>
<li>1</li>
<li>2</li>
</>
</ul>
</div>
);
console.log("jsx:", jsx);
ReactDOM.render(jsx, document.getElementById("root"));
从虚拟DOM到Fiber
首先,我们需要对虚拟DOM和Fiber的数据结构进行一个了解。
通过打印页面中JSX我们可以看到,虚拟DOM节点的type属性区分了节点的类型。如果是原生节点,type为标签名,如果是函数/类组件,type分别为函数和类声明(构造函数)。
fiber对象的设计思路和虚拟DOM类似,在其基础上增加了另外一些属性便于对Fiber树进行操作。通过打印实现的简版fiber节点,可以看到该对象有child, sibling, return属性,分别指向了当前节点的第一个子节点,下一个兄弟节点和父节点。另外也开辟了一个stateNode属性,如果是原生节点则是自身真实DOM,反之为null。(以<h3>Learn React</h3>为例)
那么我们就可以基于以上,实现一个从VNode生成Fiber的函数。child, sibling以及stateNode初始化为null, 会在遍历生成Fiber树的过程中进行赋值。
// fiber.js
export function createFiber(vnode, returnFiber) {
const fiber = {
key: vnode.key,
type: vnode.type,
props: vnode.props,
child: null,
sibling: null,
return: returnFiber,
stateNode: null,
};
return fiber;
}
渲染过程中的2个阶段
从虚拟DOM到渲染/更新真实DOM的过程中,因为考虑到Fiber可以批量有优先级地执行渲染/更新任务,整个过程分为了两个阶段:
- reconciliation/render(协调阶段)
- commit(提交阶段) 如果只考虑渲染过程,在协调阶段虚拟DOM树被遍历并生成了Fiber树,而在提交阶段Fiber树被遍历而stateNode属性中的真实DOM节点在此过程中被拿来逐级构建出要渲染的真实DOM。 为了实现对树结构的遍历,我们需要两个变量,分别指向当前正在处理的节点(协调过程中遍历),以及树的根节点(便于提交时遍历)。同时,我们可以把这两个任务分别封装成函数,并进一步封装成完整的渲染任务workLoop. 这样,我们的代码框架就有了雏形。
// ReactFiberReconciler.js
let wipRoot = null; // 根节点
let nextUnitOfWork = null; // 当前节点
function performUnitOfWork(wip) {
// 协调任务
}
function commitRoot(){
// 提交任务
}
function workLoop(IdleDeadline) {
// 封装整个过程:协调+提交
}
协调任务
在协调任务中,虚拟DOM节点树被遍历生成Fiber树。根据虚拟节点的type属性,我们可以区分以下这几种情况,以及他们的协调任务
- type值为函数:可能是函数组件/类组件
- 函数组件需要执行函数返回子节点(JSX),对其进行处理
- 类组件需要实例化类并调用render函数返回子节点(JSX),对其进行处理
- type值为字符串:是原生标签
- 需要生成真实DOM节点,更新stateNode属性
- 将props上的属性映射到DOM节点上,如果是对于props中的children,只添加文本节点
- 对于其他子节点(JSX)进行处理
- type值为Symbol(react.fragment):Fragment组件
- 直接处理子节点(JSX) 对于函数组件和类组件的区分,我们可以定义类组件继承的基类Component,在它的原型对象上声明一个属性,便于我们做区分。
我们可以把上述分情况的处理封装成函数,如下:
export function updateHostComponent(wip) {
if (!wip.stateNode) {
wip.stateNode = document.createElement(wip.type);
updateNode(wip.stateNode, wip.props); // 添加props
}
reconcileChildren(wip, wip.props.children);
}
export function updateFunctionComponent(wip) {
const { type, props } = wip;
const children = type(props); // JSX
reconcileChildren(wip, children);
}
export function updateClassComponent(wip) {
const { type, props } = wip;
const children = new type(props).render(); // JSX
reconcileChildren(wip, children);
}
export function updateFragmentComponent(wip) {
reconcileChildren(wip, wip.props.children);
}
function reconcileChildren(returnFiber, children) {
if (isStr(children)) {
// 文本节点
return;
}
const newChildren = Array.isArray(children) ? children : [children];
let previousNewFiber = null;
for (let idx = 0; idx < newChildren.length; idx++) {
const newChild = newChildren[idx];
const newFiber = createFiber(newChild, returnFiber);
if (previousNewFiber === null) {
returnFiber.child = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
把子节点的处理同样封装成函数:在这个过程中createFiber函数中置空的child, 以及sibling属性会被赋值
function reconcileChildren(returnFiber, children) {
if (isStr(children)) {
return; // 不考虑文本节点
}
const newChildren = Array.isArray(children) ? children : [children];
let previousNewFiber = null;
for (let idx = 0; idx < newChildren.length; idx++) {
const newChild = newChildren[idx];
const newFiber = createFiber(newChild, returnFiber);
if (previousNewFiber === null) {
returnFiber.child = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
整体的协调任务可以封装如下:
function performUnitOfWork(wip) {
// 更新自身
const { type } = wip;
if (isFcn(type)) {
if (type.prototype.isReactComponent) {
updateClassComponent(wip);
} else {
updateFunctionComponent(wip);
}
} else if (isStr(type)) {
updateHostComponent(wip);
} else {
updateFragmentComponent(wip);
}
// 返回下一个要更新的fiber
if (wip.child) {
return wip.child;
}
let next = wip;
while (next) {
if (next.sibling) {
return next.sibling;
}
next = next.return;
}
return null;
}
提交任务
在提交阶段,我们需要从Fiber树的根节点遍历树,不断地将stateNode中的真实DOM添加在DOM结构中的父级上,从而逐级构建要渲染的真实DOM。所谓DOM结构中的父级,是因为如果发生非原生标签嵌套的情况,直接父级上的stateNode为空。这一过程我们可以通过递归函数实现:
function commitRoot() {
commitWorker(wipRoot.child);
}
function getParentNode(fiber) {
let next = fiber.return;
while (!next.stateNode) {
next = next.return;
}
return next.stateNode;
}
function commitWorker(fiber) {
// 递归终止条件
if (!fiber) {
return;
}
const { stateNode } = fiber;
let parentNode = getParentNode(fiber);
if (stateNode) {
parentNode.appendChild(stateNode);
}
commitWorker(fiber.child);
commitWorker(fiber.sibling);
}
实现render函数
在分别实现了协调和提交的任务后,我们将这两个任务再进行一层封装。
// ReactFiberReconciler.js
export function workLoop(IdleDeadline) {
// reconciliation
while (nextUnitOfWork && IdleDeadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
// commit
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
}
在render函数中,我们创建根Fiber节点进行初始化,并将前面封装的函数作为回调传入window.requestIdleCallback方法中,在浏览器空闲时执行渲染任务(协调+提交)。至此,渲染函数完成!
// react-dom.js
import { scheduleUpdateOnFiber, workLoop } from "./ReactFiberWorkLoop";
function render(vnode, container) {
// 创建根节点
const fiberRoot = {
type: container.nodeName.toLocaleLowerCase(),
stateNode: container,
props: { children: vnode },
};
scheduleUpdateOnFiber(fiberRoot);
window.requestIdleCallback(workLoop);
}
export default { render };
总结与展望
在这次学习笔记中,总体上实现了React从虚拟DOM生成真实DOM进行初次渲染的过程。在这个过程中,通过虚拟DOM的type属性对不同类型的节点进行了区分操作。也对其中的两个阶段,协调和提交,分别进行了实现,构建了基础的Fiber结构,为后面进一步功能的实现做了铺垫。