由于JavaScript是一个单线程语言,当有js代码要执行的时候,就会停止渲染dom。
因此思考一个问题,在处理vdom的时候,如果dom结构过于复杂的话,那js的处理时间就会变长,在渲染dom的时候页面就会变得卡顿。
因此可以实现一个任务调度器,让浏览器在空闲时间去渲染dom,从而解决这个问题。
实现任务调度器
requestIdleCallback
window.requestIdleCallback() 是浏览器中提供的,可以用来监测浏览器是否有空闲时间,所以可以基于这个api来实现任务调度器
window.requestIdleCallback() 可以接受一个函数,函数中会返回一个 IdleDeadline ,通过判断 dleDeadline.timeRemaining() < 1 就能判断浏览器是否还有空闲时间
function workLoop(deadline){
let shouldYield = false
while(!shouldYield){
// 表示有空闲时间可以 to do things...
shouldYield = idleDeadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
实现react的fiber架构
这里不对fiber架构做具体的解释,有兴趣的同学可以自行搜索
采用任务调度器的方式来进行dom渲染的话,需要将dom树转化成链表的格式来进行操作,让任务去一个一个的执行。
在转化的时候定制规则:
- 先找child节点。
- 没有child节点,找sibling(兄弟)节点。
- 最后找叔叔节点,即父节点的sibling节点。
按照上述规则,一个dom的树状图可以转化成链表结构:
使用代码实现:
- 重构render方法,在render方法中给fiberOfUnit赋值:
function render(el, container){
fiberOfUnit = {
dom: container,
props: {
children: [ el ]
}
}
}
- 结合任务调度器,在空闲时间进行dom渲染
let fiberOfUnit = null
function workLoop(idleDeadline){
let shouldYield = false
// 利用while来监控空闲时间的剩余,用来决定是否要进行dom渲染
while(!shouldYield && fiberOfUnit){
fiberOfUnit = perfromFiberOfUnit(fiberOfUnit)
shouldYield = idleDeadline.timeRemaining() < 1
}
// 递归调用api,检查浏览器是否有空闲时间
requestIdleCallback(workLoop)
}
// 调用,当浏览器有空闲的时候就会调用
requestIdleCallback(workLoop)
- 首先根据上面的转化规则,可以明确的知道每一个vdom都需要child、sibling、parent以及挂载用的dom节点,但是如果直接在之前设计好的vdom上修改的话,会破坏原有的结构,所以需要新建一个对象来表示。
function perfromFiberOfUnit(fiber){
// 首先判断是否有dom,没有的话先创建dom
if(!fiber.dom){
// 1.创建dom节点
const dom = (fiber.dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type))
// 执行挂载操作
fiber.parent.dom.append(dom)
// 2.设置props
Object.keys(fiber.props).forEach(key => {
if(key !== 'children'){
dom[key] = fiber.props[key]
}
});
}
// 3.转化链表,建立起指针关系
const children = fiber.props.children
let prevChild = null
children.forEach((child, index) => {
// 因为fiber就是生成的vdom,直接修改会破坏原有的结构
let newFiber = {
type: child.type,
props: child.props,
child: null,
sibling: null,
parent: fiber,
dom: null
}
// 这里根据转化规则,第一个子节点是下一个指向的,剩下的作为第一个子节点的sibling
// 以此类推,生成最后面的链表图
if(index === 0){
fiber.child = newFiber
}else{
prevChild.sibling = newFiber
}
prevChild = newFiber
})
// 4.将下一个要执行的fiber返回回去
if(fiber.child){
return fiber.child
}
// 没有子节点时,返回兄弟节点
if(fiber.sibling){
return fiber.sibling
}
// 没有子节点也没有兄弟节点后,查找叔叔节点
return getParentSibling(fiber)
}
// 因为dom可能会嵌套多层,所以在查找叔叔节点的时候,要进行递归操作,层层往上查
function getParentSibling(fiber){
if(!fiber.parent){
return;
}
return fiber.parent.sibling || getParentSibling(fiber.parent)
}
最终转化出链表结构,对应关系如下
优化 → 统一提交:
以上代码在执行挂载操作的时候是边创建dom边挂载dom,这样做的好处是可以快速的渲染出某个节点来,但如果浏览器的空闲时间不足时,dom渲染就会卡住,页面就显示不全,给用户造成卡屏的效果。
因此可进行统一提交的优化,即先创建dom,所有的dom创建完成后再将dom挂载到根节点上。
let root = null
function render(el, container){
// ...
// 优化点,获取到转化好的vdom结构
root = nextUnitOfFier
}
function workLoop(idleDeadline){
// ....
// 优化点:当nextUnitOfFier为false时表示所有的dom都创建完成了,此时可以进行挂载操作。
if(!nextUnitOfFier && root){
commitRoot()
}
// ...
}
function commitRoot(){
commitWork(root.child)
root = null
}
// 递归操作,将child.dom和sibling.dom一层一层的挂载到parent.dom上
function commitWork(work){
if(!work) return
let parentWork = work.parent
// 兼容function component的情况。
// 因为在function component的dom节点是null,所以要循环往上查找真实的dom并挂载
// 具体详情可见下一篇关于函数组件解析的文章
while(!parentWork.dom){
parentWork = parentWork.parent
}
if(work.dom){
parentWork.dom.append(work.dom)
}
commitWork(work.child)
commitWork(work.sibling)
}