简介
在本篇文章中,我们将一起探索如何从零构建一个迷你版的React框架。通过这个过程,不仅能够加深对React内部机制的理解,还能学习到虚拟DOM、组件生命周期和状态管理等核心概念的实际应用。本文旨在深入浅出地理解React工作原理。
问题清单
Q1:怎么处理数组元素?
在渲染过程中,我们对元素el进行类型检查。当确认el为数组类型时,特殊处理。
接上述文章,我们修改一下用例
jsx
// App.jsx
import React from './core/React'
const LARGE_ARRAY_SIZE = 100
function generateLargeArray() {
return Array.from({ length: LARGE_ARRAY_SIZE }, (_, index) => index)
}
function App() {
const largeArray = generateLargeArray()
return (
<div>
<ul>
{largeArray.map((i) => (
<li key={i}>{i}</li>
))}
</ul>
</div>
)
}
export default App
jsx
// React.js
// 省略...
function render(el, container) {
if (typeof el === 'function') el = el()
// handle arrays elements
if (Array.isArray(el)) {
el.forEach((el) => render(el, container))
return
}
// 省略...
}
Q2:假如dom树非常大,渲染会发生卡顿,怎么处理?
采用分块渲染策略是一种高效的手段。这种策略充分利用了浏览器的API------requestIdleCallback,借此在浏览器空闲时间执行渲染任务。通过这种方式,渲染工作得以在可用的时间段内平均分配,从而显著减少性能瓶颈的发生,并提升用户体验。
分块渲染
根据上述思路,我们须对render函数进行相应的修改。鉴于需要实现分块渲染,我们需引入一种能够描述节点的数据结构,该结构不仅要能够描述节点,还需要支持渲染过程的中断与恢复功能。我们来看下React的解决策略------Fiber架构。
可以看下我们当前的结构树,如果我们需要遍历这个树,通常来说用链表来存储,则每个节点都需要记录
- 父节点
- 兄弟节点
- 子节点
- 挂载的dom
- 节点类型
值得注意的是,实际上在React的源代码中,这一结构比上述所述更为复杂。源码地址
此外,为了能在系统空闲时进行渲染,我们需要借助requestIdleCallback函数。这一机制使得渲染工作能够高效地利用系统的空闲周期执行,从而优化性能并提升用户体验。
所以,我们修改原有的render
函数
jsx
// React.js
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
const isTextNode =
typeof child === 'string' || typeof child === 'number'
return isTextNode ? createTextNode(child) : child
})
}
}
}
function createTextNode(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
}
}
let nextUnitOfWork = null
function render(element, container) {
// nextUnitOfWork 是 fiber 树的根节点
nextUnitOfWork = {
dom: container,
props: {
children: [element]
},
alternate: null,
parent: null,
child: null,
sibling: null
}
}
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1
}
// 闲时执行
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function updateProps(dom, props) {
const isProperty = (key) => key !== 'children'
Object.keys(props)
.filter(isProperty)
.forEach((name) => {
dom[name] = props[name]
})
}
function createDom(fiber) {
const dom =
fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type)
updateProps(dom, fiber.props)
return dom
}
function reconcileChildren(wipFiber, elements) {
let prevSibling = null
let index = 0
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
function updateFunctionComponent(fiber) {
// Run the function component to get the children
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
if (!fiber.parent.dom) {
// function component 的 fiber 没有 dom
// 如果 fiber 的父节点没有 dom,就找到最近的有 dom 的父节点
while (!fiber.parent.dom) {
fiber.parent = fiber.parent.parent
}
}
fiber.parent.dom.appendChild(fiber.dom)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
const children = isFunctionComponent
? [fiber.type(fiber.props)]
: fiber.props.children
reconcileChildren(fiber, children)
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Q3:当浏览器资源被充分占用且缺乏空闲时间,而渲染进程尚未完成时,应该怎么处理?
为了优化渲染过程并有效管理资源消耗,可以采取一种策略,即首先构建一个Fiber树,Fiber树构建完成后,可以将其在适当的时间点一次性挂载到DOM上。
统一提交
为了解决上述问题,我们需要设计一个函数,来完成最后一次性提交,这就是React
中的commitWork
。
因为我们需要在最后才一次性挂载,所以需要记录fiber树的根节点
jsx
// React.js
// 省略...
// 记录fiber树的根节点
let root = null
function render(element, container) {
// nextUnitOfWork 是 fiber 树的根节点
nextUnitOfWork = {
dom: container,
props: {
children: [element]
},
alternate: null,
parent: null,
child: null,
sibling: null
}
// new feature
root = nextUnitOfWork
}
function workLoop(deadline) {
// 省略...
// 当没有下一个工作单元时,提交整个 fiber 树
if (!nextUnitOfWork && root) {
commitRoot()
}
requestIdleCallback(workLoop)
}
function commitRoot() {
// 递归提交整个fiber树到DOM
commitWork(root.child)
// 重置工作单元
root = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
let fiberParent = fiber.parent
// handle function component
while (!fiberParent.dom) {
fiberParent = fiberParent.parent
}
if (fiber.dom) {
fiberParent.dom.appendChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
总结
我们成功地实现了元素在界面中的正确渲染,并对渲染过程进行了优化,采用了分块渲染与统一提交的策略。通过引入Fiber架构,我们能够有效地追踪当前的元素结构树,同時利用一个指针来引用整个渲染过程的根节点。这种方法不仅提高了渲染效率,而且为动态更新提供了支持。
在后续章节中,我们将详细介绍
- 如何实现属性(props)的更新机制
- useState和useEffect