1. 实现任务调度器
(1)当我们执行一段js逻辑时,当运行的数据过多时,dom树就会特别大从而导致渲染卡顿,因为js是一个单线程语言,它在执行一个较长的逻辑时,就会阻塞我们一个后续的渲染,那么如何解决?
我们可以通过浏览器提供的一个API:requestIdleCallback
(2)我们通过调用requestIdleCallback,通过接收它传来的参数 deadline,调用deadline.timeReamaining()来查询浏览器的空余时间。我们就可以通过这个空余时间,将大任务拆分为多个小任务完成。
演示代码:
let taskId = 1
function workLoop(deadline) {
// 获取浏览器空余时间
console.log('+++++++',deadline.timeRemaining());
taskId++
let shouldYield = false;
while(!shouldYield) {
console.log('run task',taskId);
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
2. 实现fiber架构
(1)我们的dom结构是一棵树,那么如何做到每次渲染几个节点,在下次执行的时候依然从之前的位置执行?我们可以将整个dom树转化为链表结构,并将其分为3个节点,孩子,兄弟,父亲节点。从上往下依次查找进行转化,如图:
流程关系:跟据树形结构将其转化为链表结构。先渲染A节点,看A节点是否有孩子节点,到B节点渲染,继续找孩子节点,到d节点,发现d节点没有孩子节点,找兄弟节点e,然后找叔叔节点c,然后孩子f,兄弟g节点,最后结束。采用先找child -> sibling兄弟 -> 叔叔的关系去进行链表结构。
那么,问题来了,我们是浏览器渲染时,现将整个树转化为链表结构再进行渲染处理,还是边转化着边处理,毋庸置疑,,第二种方法性能是最优的。
(2)将任务调度器引入到我们的代码中,在任务调度器的while循环里执行我们的任务nextWorkOfUnit,定义performWorkOfUnit方法用于执行渲染节点的逻辑。它会返回一个新的nextWorkOfUnit,这个就是一个新的指针,会指向在上一次浏览器的空余时间内执行任务的最后一项,在浏览器最新的时间空余出来时,接着最新的指针往下执行。
(3)接着我们要做的就是在performWorkOfUnit方法中,1.创建dom,2.处理props,3.将树形结构转化为链表结构,设置好指针,4.返回下一个要执行的任务。
1. 创建dom
function createDom(type) {
return type === 'TEXT_ELEMENT' ?
document.createTextNode("") :
document.createElement(type)
}
2. 处理props
function updateProps(props,dom) {
for (const key in props) {
if(key !== 'children') {
dom[key] = props[key]
}
}
}
3. 将树形结构转化为链表结构
function initChildren(fiber) {
const children = fiber.props.children
let prevChild = null
children.forEach((child,index) => {
// 由于我们要给孩子节点定义父亲等属性,所以定义新的对象,这样可以不直接破坏之前vdom的结构
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 = newFiber
});
}
4. 返回下一个要执行的任务
// 4.返回写一个要执行的任务
if(fiber.child) {
return fiber.child
}
if(fiber.sibling) {
return fiber.sibling
}
return fiber.parent?.sibling
(4)对于render函数的优化以及修改
function render(el,container) {
nextWorkOfUnit = {
dom:container,
props:{
children:[el]
}
}
}
(5)任务调度器的修改
let nextWorkOfUnit = null
function workLoop(deadline) {
let shouldYield = false;
while(!shouldYield && nextWorkOfUnit) {
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
(6)总结:1. 递归代码在较大数据量的情况下比较容易造成卡顿。递归代码需要一次性执行完,而且数据量特别大的时候容易出现爆内存。为了解决递归的问题,一般都会将递归代码转成遍历代码。对于 dom 树则需要转成链表结构进行遍历,边执行边转换的技巧也比较优雅。再结合指针,使得链表遍历可以随时停止并恢复。2.使用 requestIdleCallback 实现调度器,利用浏览器空闲时间进行 dom 元素的渲染。