xdm,又要到饭了,又更新代码了!
总结一下上一篇完成的内容,
- 实现了mini fiber 的第一版,
有兴趣的可以点这里查看fiber 的迷你版实现 (1)
这篇将会更进一步实现fiber树。
1. 实现 Fiber 树结构
为了支持可中断的渲染,我们需要引入 Fiber 树 的概念。每个 Fiber 节点代表一个组件实例,包含组件的相关信息和状态。
Fiber 节点定义
js
class FiberNode {
constructor(type, props, parent = null, sibling = null, child = null) {
this.type = type; // 组件类型
this.props = props; // 组件属性
this.parent = parent; // 父节点
this.sibling = sibling; // 兄弟节点
this.child = child; // 子节点
this.effectTag = null; // 标记需要执行的操作(如更新)
this.state = null; // 组件的状态(useState)
this.hooks = []; // 组件的 Hook 列表
this.currentHook = 0; // 当前 Hook 的索引
}
}
Fiber 树的构建
在渲染过程中,我们会构建和遍历 Fiber 树。Fiber 树允许我们在渲染任务之间保存和恢复状态,从而实现可中断的渲染。
2. 拆分渲染任务
为了实现可中断的渲染,我们需要将渲染过程拆分为多个可中断的小任务。每次在浏览器的空闲时间(通过 requestIdleCallback
或其回退方案)执行一部分任务,然后暂停,等待下一次空闲时间继续执行剩余任务。
渲染的过程自顶向下,所以我们先创建一个fiber 的root 节点,根据上面的fiber 定义,我们可以先得出下面的代码
js
let currentRoot = null
function renderElement(vdom, container) {
// 创建根 Fiber
const rootFiber = new FiberNode('ROOT', {children: vdom}, null, null, null)
rootFiber.stateNode = container
rootFiber.alternate = currentRoot
if (currentRoot) {
currentRoot.alternate = rootFiber
}
currentRoot = rootFiber;
nextUnitOfWork = rootFiber;
requestHostCallback(workLoop);
}
- nextUnitOfWork 设置下一个需要处理的fiber,首次进入就是根 fiber
- requestHostCallback 上一节已经完成了,其就是对 requestIdleCallback 的装饰。
- workLoop,一个函数, 接受一个参数。 参数可以获取/检查每一个frame剩余/空闲时间,决定是否继续执行更多的任务
2.1 实现 workLoop
js
function workLoop(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) {
requestHostCallback(workLoop);
} else {
commitRoot();
}
workLoopScheduled = false;
}
上面的workLoop函数循环的检查是否还有任务没有完成而且确认当前frame是否还有空闲时间。如果2个条件都满足,就会开始处理当前的fiber(组件),从函数代码
js
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
可以知道,performUnitOfWork接受一个参数,这个参数就是待处理的当前fiber。performUnitOfWork 的返回值就是下一个需要处理的fiber。这里指的下一个fiber,就是当前fiber的子fiber,但是如果当前fiber 没有子fiber,那么返回当前fiber的兄弟fiber,如果也没有兄弟fiber,那么就返回null, 表示当前fiber处理完毕。
来实现一下performUnitOfWork。需要完成,
- 如果当前的fiber是根fiber,则可以使用fiber.props.children获取其子组件,
props.children
通常包含的是组件的子组件。 - 如当前的fiber类型是一个函数函数,那么就表明当前fiber对应函数组件, 那么运行这个函数组件获取其虚拟dom(子组件),fiber(fiber.props), 运行返回值就是其虚拟dom(
props.children
通常包含的是组件的子元素/组件)。 - 获取子组件 之后,就开始进行调和(reconcile),reconcile具体实现在下面
- 调和后,会返回这一层级的第一个子组件fiber进行performUnitOfWork(下一个需要处理工作单元, next unit of work)
- 如果没有子组件fiber,则尝试返回兄弟组件fiber,如果没有兄弟组件fiber,则进行回溯返回父fiber,以此类推直至根fiber,则返回null,遍历结束。
js
function performUnitOfWork(fiber) {
workInProgress = fiber
if (fiber.type === 'ROOT') {
const children = fiber.props.children
reconcileChildren(fiber, children)
} else if (typeof fiber === 'function') {
const component = fiber.type
const props = fiber.props
workInProgress.hooks = workInProgress.hooks || []
workInProgress.currentHook = 0
const children = component(props)
reconcileChildren(fiber, children)
} else {
reconcileChildren(fiber, fiber.props.children)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while(nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent;
}
return null
}
进行到这里我们开始实现调和算法,
js
function reconcileChildren(fiber, children)
{
const oldFiberMap = {}
let oldFiber = fiber.alternate ? fiber.alternate.child : null
while(oldFiber) {
const key = oldFiber.key || oldFiber.index
oldFiberMap[key] = oldFiber
oldFiber = oldFiber.sibling
}
let prevSibling = null
let newFiber= null
let index = 0
for(; index < children.length; index++) {
const child = children[index]
const key = child.key || index
const matchedOldFiber = oldFiberMap[key]
if (matchedOldFiber) {
newFiber = createWorkInProgress(matchedOldFiber, child.props)
newFiber.key = key
newFiber.effectTag = 'UPDATE'
delete oldFiberMap[key]
} else {
newFiber = new FiberNode(child.type, child.props, fiber)
newFiber.key = key
newFiber.effectTag = 'PLACEMENAT'
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
}
Objects.values(oldFiberMap).forEach(fiberToDelete => {
fiberToDelete.effectTag = 'DELETION'
})
}
调和函数目的在于新的 vDOM 和旧的 Fiber 树之间建立联系,通过创建、更新和复用 Fiber 节点来优化渲染过程。这样可以确保 UI 的高效更新,减少不必要的 DOM 操作,从而提高性能和用户体验。reconcile的调用,
- 决定了performUnitOfWork 下一个需要处理的fiber,
- 这里使用了链表(prevSibling),使用sibling 记录当前处理的fiber的sibling。方便第三步进行遍历
- 处理fiber的顺序遵从相同等级的所有组件先创建对应的fiber,进行newProps的更新。接着返回这一层级第一个组件成为 next unit of work,接着第二个,以此类推。
这里选择使用链表的好处
- 高效深度优先遍历
使用链表结构使得 Fiber 树可以方便地进行深度优先遍历。这种遍历方式对于渲染和更新组件树非常重要,因为它允许逐层处理组件,从父组件到子组件,再到更深层的嵌套组件。
- 子节点和兄弟节点的指针 :每个 Fiber 节点通过
child
指针指向其第一个子节点,通过sibling
指针链接到下一个兄弟节点。这种结构使得遍历时无需使用额外的数据结构(如栈或队列),从而提高了遍历的效率。
- 支持可中断和可恢复的渲染
Fiber 的设计目标之一是实现可中断的渲染,这意味着渲染过程可以在任何时间点暂停,并在稍后的时间点恢复,以保证用户界面的响应性。
- 链表结构的优势 :通过
child
和sibling
指针,Fiber 可以在任何节点处暂停渲染,然后从该节点继续。这种灵活性在处理长时间运行的渲染任务时尤为重要,因为它允许高优先级的任务(如用户交互)打断当前的渲染任务。 - 工作单元的连续性:链表结构确保了每个工作单元(即每个 Fiber 节点)都可以独立处理和调度,不需要依赖于整个组件树的状态。这使得 Fiber 更容易管理和恢复中断的任务。
接着来完善 createWorkInProgress 函数,这个函数就会根据传入的fiber尝试在当前的fiber树获取对应的fiber(判断key) ,如果相同,则复用。如果没有,则创建。
js
function createWorkInProgress(currentFiber, newProps) {
let workInProgressFiber = currentFiber.alternate
if (!workInProgressFiber) {
workInProgressFiber = new FiberNode(currentFiber.type, newProps, currentFiber.parent)
workInProgressFiber.alternate = currentFiber
currentFiber.alternate = workInProgressFiber
} else {
workInProgressFiber.props = newProps
workInProgressFiber.effectTag = null
}
workInProgressFiber.parent = currentFiber.parent
workInProgressFiber.child = null; workInProgressFiber.sibling = null;
workInProgressFiber.stateNode = currentFiber.stateNode;
workInProgressFiber.hooks = [...currentFiber.hooks];
workInProgressFiber.currentHook = currentFiber.currentHook;
return workInProgressFiber;
}
createWorkInProgress 根据传入的fiber尝试复用对应的fiber,如果不存在,才创建。接着根据新的props进行更新,最后复制其他数据比如 hooks, child, sibling, etc...
这一章节讲解了从0构建fiber树的创建的过程,包括其中使用的数据结构以及使用这样的数据结构有什么好处。
下一篇将继续完成fiber树更新真正dom的相关实现
如果这样的长度/强度你觉得可以接受,觉得有帮助,可以继续阅读下一篇,实现一个 Mini React:核心功能详解 - fiber 的迷你版实现(3)。
如果文章对你有帮助,请点个赞支持一下!
啥也不是,散会。