「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
实现
我们将分为8步,一步一步的实现一个小型的React
- 实现createElement函数
- 实现render函数
- Currnet Mode模式
- fibers
- render和commit阶段
- 协调器
- function组件
- hooks
fibers
将整个渲染任务分成一个一个小任务,我们需要一个种数据结构:fiber树,在react中同时只会存在两棵fiber树,一个是当前展示在屏幕的current tree,另一个是正在构建的workInProcess tree。当接收到更新任务时,就会根据传入的JSX构建fiber树,每个dom节点对应一个fiber,也就对应着一个小任务。
当在内存中构建workInProcess tree,构建完成时,将current tree等于workInProcess tree,就完成了一次页面更新。
假设我们要渲染的JSX如下:
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
在开始渲染的时候,首先会创建root fiber并把它赋值给nextUnitOfWork,当存在nextUnitOfWork就会执行performUnitOfWork方法,该方法会做三件事:
- 如果dom不存在就创建dom节点
- 对于每个子节点创建fiber节点
- 返回下一个任务
对于每个fiber节点,都会child指针,指向子节点;如果有兄弟节点,还会指sibling指针,指向兄弟节点。每个fiber节点还会有return指针,指向父节点。如下图:
有了这个数据结构,我们就可以很容易找到下一个任务,当我们完成创建一个fiber任务,如果它存在child,那么child就是下一个任务。例如上面,当我们完成div fiber任务,下一个任务就是h1 fiber。
如果不存在child,那么就将sibling当做下一个任务。例如p fiber没有child,我们将a 做为下一个任务
如果即没有child也没有sibling,我们将返回到parent中寻找parent的sibling,直到返回到root fiber,此时就说明我们已经完成了构建fiber tree的工作。
下面我们来实现一个代码吧 我们把create dom单独抽成方法,后面还会用到,在render函数里,我们将root赋值给nextUnitOfWork,当浏览器有空闲时间,就会执行workLoop方法,然后从root开始第一个任务。
function createDom(fiber) {
const dom =
fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type);
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom;
}
function render(element, container) {
nextUnitOfWork = {
dom: continer,
props: {
children: [element]
},
}
}
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) { // 存在下一个任务并且存在空闲时间
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
下面我们实现一下performUnitOfWork方法 第一步:如果fiber中不存在dom节点,就创建dom节点并将节点放入parent的dom节点中
function performUnitOfWork(fiber) {
// 创建dom
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// TODO 为每个child创建fiber节点
// TODO 返回下一个任务
}
第二步:为每个child创建fiber节点
function performUnitOfWork(fiber) {
// 创建dom
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// 为每个child创建fiber节点
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) { // 如果是首个child,将赋值为child,后面的将赋值为sibling
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// TODO 返回下一个任务
}
第三步:查找下一个任务,首先尝试fiber.child,然后是sibling,都没有就返回parent继续寻找
function performUnitOfWork(fiber) {
// 创建dom
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// 为每个child创建fiber节点
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
if (index === 0) { // 如果是首个child,将赋值为child,后面的将赋值为sibling
fiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
// 返回下一个任务
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
总结
- 要实现任务划分,依赖于fiber数据结构
- 开始渲染时会将root赋值给nextUnitOfWork,然后当浏览器有空闲时间时,开始执行performUnitOfWork,开始构建fiber tree
- performUnitOfWork做了三件事情:
- 创建dom节点,并将节点append 到parent节点中;
- 为每个child创建fiber节点;
- 寻找下一个任务并返回