Fiber的简单实现

421 阅读3分钟

requestIdleCallback

  1. 功能: 利用浏览器的空余时间执行任务,如果有更高优先级的任务要执行时,当前执行的任务可以被终止,优先执行高级别任务。
requestidleCallback(function(deadline){
    deadline.timeRemaining() // 获取浏览器空余时间
})
  1. 浏览器空余时间: 页面是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿

1s 60帧,每一赖分到的时间是1000/60~16 ms,如果每一帧执行的时间小于16ms,就说明浏览器有空余时间

如果任务在剩余的时间内没有完成则会停止任务执行,继续优先执行主任务,也就是说requestldleCallback 总是利用浏览器的空余时间执行任务

  1. 例子:
<style>
    .playground{
        background: palevioletred;
        padding:20px;
        margin-bottom: 20px;
    }
</style>
<body>
    <div class="playground" id="play">playground</div>
    <button id="work">work</button>
    <button id="interaction">interaction</button>
</body>
<script>
    let play = document.querySelector('#play');
    let work = document.querySelector('#work');
    let interaction = document.querySelector('#interaction');
    let count = 100;

    let expensive = id => {
        while(count > 0 && id.timeRemaining() > 1){
            count--;
            console.log(count)
        }
        console.log('执行完成')
    }

    work.addEventListener('click',()=>{
        requestIdleCallback(expensive)
    })

    interaction.addEventListener('click',()=>{
        play.style.background = 'blue'
    })
</script>

概念

  1. 问题 React 16 之前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿,非常的影响用户体验。

核心问题:递归无法中断,执行重任务耗时长。JavaScript 又是单线程,无法同时执行其他任务,导致任务延迟页面卡顿,用户体验差。

  1. 解决

    1.利用浏览器空闲时间执行任务,拒绝长时间占用主线程

    2.放弃递归只采用循环,因为循环可以被中断

    3.任务拆分,将任务拆分成一个个的小任务

  2. 实现思路 在 Fiber 方案中,为了实现任务的终止再继续,DOM比对算法被分成了两部分:

    1.构建 Fiber (可中断)

    2.提交 Commit (不可中断)

    DOM 初始渲染: virtualDOM -> Fiber -> Fiber[]->DOM

    DOM 更新操作: newFiber vs oldFiber > Fiber[] ->DOM

  3. Fiber 对象

{
    type 节点类型(元素,文本,组件)(具体的类型) 
    props 节点属性 
    stateNode 节点 DOM 对象组件实例对象 
    tag 节点标记(对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)

    effects 数组,存储需要更改的 fiber 对象 

    effectTag 当前 Fiber 要被执行的操作(新增,删除,修改) 

    parent 当前 Fiber 的父级 Fiber 
    child 当前 Fiber 的子级 Fiber 
    sibling 当前 Fiber 的下一个兄弟 Fiber 

    alternate Fiber 备份 fiber 比对时使用
}

实现

  1. 创建任务队列
const root = document.createElement('root');
const jsx = (
    <div />
)
render(jsx, root)
const taskQueue = createTaskQueue()
const render = (element, dom) => {
    taskQueue.push({
        dom,
        props: { children: element }
    })
    requestIdleCallback(preformTask)
}
const createTaskQueue = () => {
    const taskQueue = [];
    return {
        push: item => taskQueue.push(item),
        pop: () => taskQueue.shift(),
        // 判断队列是否还有任务
        isEmp
    }
}
  1. 实现认读调度
const preformTask = deadline => {
    // 执行任务
    workLoop(deadline)
    // 判断任务是否存在且队列中是否还有任务
    if(subTask || !taskQueue.isEmpty()){
        requestIdleCallback(preformTask)
    }
}
const workLoop = deadline => {
    if(!subTask){
        subTask = getFirstTask()
    }
    while(subTask && deadline.timeRemaining() > 1){
        subTask = executeTask(subTask)
    }
}
  1. 执行任务
// 构建根节点Fiber对象
const getFirstTask = () => {
    const task = taskQueue.pop()
    return {
        props: task.props,
        stateNode: task.dom,
        tag: 'host_root',
        effects: [],
        child: null,
        alterbate: task.dom.__rootFiberContainer
    }
}
// 构建子节点Fiber对象
const executeTask = () => {}
  1. 实现初始渲染、更新节点
const commitAllWork = fiber => {
    fiber.effects.forEach(item => {
        if(item.effectTag === 'placement'){
            item.parent.stateNode.appendChild(item.stateNode)
        } else if(item.effectTag === 'update'){
            if(item.type === iten.alternate.type){
                updatsNodeElement(item.stateNode, item, item.alternate)
            }else{
                item.partent.stateNode.replaceChild(
                    item.stateNode,
                    item.alternate.stateNode,
                )
            }
        }
    })
    // 备份旧的Fiber对象
    fiber.stateNode.__rootFiberContainer = fiber;
}