上节我们使用递归的方式完成了将虚拟dom树转换为真实节点,但如果dom树的子节点过多时,浏览器就会出现卡顿。
原因是JS是单线程的,我们执行的任务过多,阻塞了渲染引擎,导致出现卡顿。
所以我们需要对任务进行拆分,JS 执行一部分,然后渲染引擎渲染一部分,完成之后,JS 再继续执行,渲染引擎再渲染。
浏览器给我们提供了一个钩子函数 requestIdleCallback
在空余时间执行我们想要的逻辑
requestIdleCallback
requestIdleCallback(callback[, options])
callback
是需要执行的任务,接收一个 IdleDeadline 对象作为参数。IdleDeadline 包含 2 个重要字段
- didTimeout,布尔值,表示任务是否超时
- timeRemaining() ,用于获取当前帧的剩余时间
options
是一个可选参数,目前只有一个值 timeout
,表示如果超过这个时间,任务还没有执行,则强制执行任务,不需要等待空闲时间。
实现任务调度器
我们将大任务拆为多个task,再利用 requestIdleCallback
在浏览器空闲时去执行每个task,这样就可以实现边执行JS边渲染的效果了
我们定义一个变量 shouldYield
来控制是否去执行task,在空闲时间不足时跳过任务的执行
js
let taskId = 0;
function workLoop(deadline) {
taskId++;
let shouldYield = false;
while (!shouldYield) {
if (deadline.timeRemaining() < 0) {
shouldYield = true;
}
//模拟task的执行
console.log(`run task${taskId}`);
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
实现fiber架构
首先我们定义一个变量代表我们的task:let nextWorkOfUnit = null;
然后定义执行任务的函数,参数为task,它返回下一个需要执行的task:
function performWorkOfUnit(fiber) {}
这样便可以使用任务调度器去代替之前的递归操作了:
js
function workLoop(deadline) {
let shouldYield = false;
while (!shouldYield && nextWorkOfUnit) {
shouldYield = deadline.timeRemaining() < 1;
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
接下来我们去实现 performWorkOfUnit
:
首先肯定是要执行上节的render
中的逻辑,将构建的 vdom 转换为真实dom,即创建dom和处理props,我们将这两个步骤抽离出来:
js
function createDom(type) {
return type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(type);
}
function updateProps(dom, props) {
Object.keys(props).forEach((prop) => {
if (prop !== "children") {
dom[prop] = props[prop];
}
});
}
performWorkOfUnit
需要返回下一个执行的task,那我们该如何去知道下一个任务该执行什么呢?这里便需要我们将dom树去转换为链表,再通过链表去寻找下一个任务。
定义链表的规则为:
- 子节点child
- 兄弟节点sibling
- 叔叔节点parent.sibling
a --> b --> d --> e --> c --> f --> g --> 结束
我们也将这一步抽离为单独的函数:
js
function initChildren(fiber) {
const children = fiber.props.children;
let prevChild = null;
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
child: null,
parent: fiber,
sibling: null,
dom: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
prevChild.sibling = newFiber;
}
prevChild = child;
});
}
为了不破坏虚拟dom的结构,我们定义了一个新的对象newFiber
,其dom属性为append的位置,同时定义变量prevChild
来保存上一个子节点的内容,用来设置sibling的指向
js
function performWorkOfUnit(fiber) {
if (!fiber.dom) {
//创建dom,设置添加的位置
const dom = (fiber.dom = createDom(fiber.type));
//添加dom
fiber.parent.dom.append(dom);
//处理props
updateProps(dom, fiber.props);
}
//转换链表
initChildren(fiber);
//返回下一个任务 :1.child 2.sibling 3.parent.sibling
return fiber.child
? fiber.child
: fiber.sibling
? fiber.sibling
: fiber.parent.sibling
? fiber.parent.sibling
: null;
}
最后在render
函数中,初始化变量nextWorkOfUnit
,这样我们的fiber架构就实现完成了
function
nextWorkOfUnit = {
dom: container,
props: {
children: [el],
},
};
}
代码参考链接 : mini-react
参考文章
- 《从简单中窥见高端,彻底搞懂任务可中断机制与任务插队机制》---React知命境第42篇