实现一个精简React -- 解决复杂DOM渲染卡顿:实现任务调度器与fiber架构(3)

134 阅读4分钟

由于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树转化成链表的格式来进行操作,让任务去一个一个的执行。

image.png

在转化的时候定制规则:

  1. 先找child节点。
  2. 没有child节点,找sibling(兄弟)节点。
  3. 最后找叔叔节点,即父节点的sibling节点。

按照上述规则,一个dom的树状图可以转化成链表结构: image.png

使用代码实现:

  1. 重构render方法,在render方法中给fiberOfUnit赋值:
function render(el, container){
    fiberOfUnit = {
        dom: container,
        props: {
            children: [ el ]
        }
    }
}
  1. 结合任务调度器,在空闲时间进行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)
  1. 首先根据上面的转化规则,可以明确的知道每一个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)
}

最终转化出链表结构,对应关系如下

image.png

优化 → 统一提交:

以上代码在执行挂载操作的时候是边创建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)
}

项目源码:github.com/Cuimc/mini-…