前因
上节的递归调用会出现一个问题,也就是一旦开始渲染,就不能停止了,直到渲染出完整的树结构。也就是说会造成主线程被持续占⽤,造成的后果就是主线程上的布局、动画等周期性任务就⽆法立即得到处理,造成视觉上的卡顿,影响⽤户体验。
在浏览器中,页面是一帧一帧绘制出来的,主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次,在这一帧中浏览器要完成JS脚本执行、样式布局、样式绘制,如果在某个阶段执行时间很长,超过 16.6ms,那么就会阻塞页面的渲染,从而出现卡顿现象,也就是常说的掉帧。
以上来自
如果dom树特别大, 就会导致 js运行时间长 导致 绘制时间往后拖延, 给人视觉上就是 卡顿的效果
情况类似如下
了解决堵塞问题 我们引入了 并发模式(红绿灯)和 nextUnitOfWork fiber (将 react 递归渲染 切割为 单独一个小块小块 类似计程车 🚗)
类似如下
使用增量渲染(把渲染任务拆分成块,匀到多帧),将把工作分解成小单元,在完成每个单元之后,如果还有其他任务需要完成,我们将让浏览器中断渲染,也就是经常听到的fiber。
关键点:
-
增量渲染
-
更新时能够暂停,终止,复⽤渲染任务
-
给不同类型的更新赋予优先级
并发模式 红绿灯🚥
实现fiber的核心是window.requestIdleCallback(),window.requestIdleCallback()⽅法将在浏览器的空闲时段内调⽤的函数队列。关于window.requestIdleCallback()请点击查看文档。
你可以把 requestIdlecallback 想象成一个 setTimeout,但是浏览器不会告诉你它什么时候运行,而是在主线程空闲时运行回调。
window.requestIdleCallback将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
我们使用window.requestIdleCallback来实现代码
// 下一个工作节点
let nextUnitOfWork = null as FiberProps | null | undefined;
function workLoop(deadline: any) {
let shouldYield = false
// 如果存在下一个工作节点 就继续 执行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
Fiber
将 虚拟DOM 递归渲染顺序 变为 后序遍历 遍历相关 可看文章 从 React 源码中学到的非递归先序遍历和后序遍历算法
fiber树结构格式为
// 单个工作格类型
export type FiberProps = VDOMProps & {
/** 真实dom节点*/
dom: Element | null;
/** 父节点工作格 */
parent?: FiberProps;
/** 子节点工作格 父节点下第一个子节点 */
child?: FiberProps;
/** 相邻工作格 相邻的下一个兄弟节点 */
sibling?: FiberProps;
/** 属性 */
props: Omit<VDOMProps, 'children'> & {
children: FiberProps[]
}
}
通过 parent child sibling 形成链表, 可以找到 下一个工作节点 nextUnitOfWork
顺序为 先父节点 --- 第一个子节点 ---- 是否有子节点 继续 ---- 无子节点 就 兄弟节点 ---- 无兄弟节点
fiber生成顺序为 A B D E C F
初始化render
初始化 render 和 第一个 nextUnitOfWork 工作节点
import type { VDOMProps, FiberProps, Element } from '../shared';
function createDom(fiber: FiberProps): Element {
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = (key: string) => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
// @ts-ignore
dom[name] = fiber.props[name]
})
return dom
}
/**
* 初始化第一个fiber节点
* */
function render(vDom: VDOMProps , container: Element) {
nextUnitOfWork = {
dom: container,
props: {
children: [vDom],
},
} as FiberProps
}
// 下一个工作节点
let nextUnitOfWork = null as FiberProps | null | undefined;
创建fiber
1、创建fiber 当前fiber 生成dom
function performUnitOfWork(fiber: FiberProps): FiberProps | null | undefined {
// 当前节点 生成dom
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 当前节点 插入dom中
if (fiber.parent?.dom) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
2、对子节点进行遍历 生成新的fiber
child 代表第一个 子节点, sibling代表 下一个兄弟元素
function performUnitOfWork(fiber: FiberProps): FiberProps | null | undefined {
...
// children 生成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) {
fiber.child = newFiber
} else {
if (prevSibling) {
(prevSibling as FiberProps).sibling = newFiber
}
}
prevSibling = newFiber
index++
}
}
3 返回下一个工作节点
function performUnitOfWork(fiber: FiberProps): FiberProps | null | undefined {
...
// 如果有子节点工作格, 返回子工作格
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果有兄弟节点 返回相邻兄弟工作格
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 如果没有 返回上一级
nextFiber = nextFiber.parent as FiberProps;
}
}
以上 就实现了 将VDOM 生成 单个工作格Fiber 链表, 并进行渲染