React的fiber构架,浅入深出

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

1. 出现背景

js是单线程的,执行js脚本的时候,js引擎和页面渲染引擎在同一个渲染线程,GUI 渲染和js执行 两者是互斥的。正好React渲染虚拟dom是递归的找出需要变动的节点,然后同步更新它们, 一气呵成(这个过程 React称为Reconcilation)。这样当页面复杂(描述的虚拟节点也会复杂)。就会导致js一直占用渲染进程,而导致GUI无法进行,使得页面页面卡顿,或者用户触发的事件得不到响应

因此需要一种手段可以中断Reconcilation,让渲染进程能做一些其他的事情。为此fiber构架应运而生。

2. 前置知识

2.1 requestAnimationFrame

该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

2.2 requestIdleCallback

方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

requestIdleCallback执行示意图.png

模拟工作分片,使用requestIdleCallback进行调度

  1. 首先是将一个大的工作分成几个小块的工作
  2. 然后处理工作,进入工作队列循环
    • 检查浏览器是否有剩余时间能处理工作分片,
    • 如果没有,有还有任务分片,就使用requestIdleCallback向浏览器申请处理时间,进入事件处理等待队列,
    • 如果有,并且也存在任务分片就从工作队列弹出一个工作分片进行处理
// 这个是为了增加程序运行事件,可以动态的延长工作耗时
function weak(duration){
    let start = Date.now();
    while(start + duration > Date.now()){
        console.log('weak')
    }
}
// 工作分片,这个工作本来是一个整体
const works = [
    ()=>{
        console.log('A1开始')
        weak(20)
        console.log('A1结束')
    },
    ()=>{
        console.log('B1开始')
        console.log('B1结束')
    },
    ()=>{
        console.log('C1开始')
        console.log('C1结束')
    },
]
function workLoop(IdleDeadline ){
    console.log('本帧的剩余时间',parseInt( IdleDeadline.timeRemaining() ))
    while(IdleDeadline.timeRemaining()>0 && works.length>0){
        preformUnitOfWork()
    }
    if(works.length>0){
        console.log('只剩下'+IdleDeadline.timeRemaining()+',时间片即将到期,等待下次申请')
        requestIdleCallback(workLoop)
    }
}
function preformUnitOfWork(){
    let work = works.shift();
    work();
}
requestIdleCallback (workLoop)
复制代码

以上就是一个利用requestIdleCallback,对任务分片调度的处理

2.3 使用MessageChannel模拟requestIdleCallback

因为requestIdleCallback目前还是一个实验阶段的功能,支持性不是很好,所以我们必须要使用另一种方式来实现requestIdleCallback的功能,来获取浏览器每帧空余时间处理事件的能力,react中就是使用的MessageChannel配合requestAnimationFrame模拟的requestIdleCallback

MessageChannel基本使用

const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;

port1.addEventListener('message',(event)=>{
    console.log(event.data)
})
复制代码

重要参考: requestAnimationFrame中的回调函数接受一个参数,该参数与performance.now()的返回值相同(表现上近似),可以这么理解,打开或刷新页面开始执行代码的那一刻为0以这个为参考点,performance.now()就代表执行到这里时,花了多少时间

模拟requestIdleCallback具体代码:

window.myRequestIdleCallback = function(callback,options){
    requestAnimationFrame((DOMHighResTimeStamp)=>{
        console.log(`DOMHighResTimeStamp:${DOMHighResTimeStamp}-${performance.now()}`)
        myRequestIdleCallback.IdleDeadline = DOMHighResTimeStamp + myRequestIdleCallback.activeAnimationTime
        myRequestIdleCallback.peedingCallback = callback;
        myRequestIdleCallback.channel.port1.postMessage('start');
    })
}
myRequestIdleCallback.activeAnimationTime = 1000/60; // 每一帧的时间 ms
myRequestIdleCallback.IdleDeadline; // 这一帧的截止时间
myRequestIdleCallback.tiemRemaing = ()=> myRequestIdleCallback.IdleDeadline - performance.now(); // 执行到此语句还有多少空余时间剩余
myRequestIdleCallback.channel = new MessageChannel();
myRequestIdleCallback.channel.port2.onmessage = function(event){
    // 当收到消息的时候表示,浏览器已经空闲,处理该任务
    const currentTime = performance.now();//运行到当前回调函数的时刻
    // 如果deadline为true,意味着当前时间已经超出了每一帧的截止时间,也就等同于本帧没有任何时间可以处理回调函数,此帧过期
    const isDeadLine = currentTime > myRequestIdleCallback.IdleDeadline
    if( !isDeadLine || myRequestIdleCallback.tiemRemaing()>0){
        if(myRequestIdleCallback.peedingCallback){
            myRequestIdleCallback.peedingCallback({
                timeRemaining:myRequestIdleCallback.tiemRemaing
            });
        }
    }
}
复制代码

代码分析:

  1. 利用requestAnimationFrame的回调函数接受的参数得到执行到该条语句时的时间,然后通过DOMHighResTimeStamp + 每帧消耗的时间长度得到的就是这一帧执行完的时间
  2. 使用myRequestIdleCallback的port1去发送消息,当port2接受到消息就认为,此时浏览器在此有空闲时间。
  3. port2接受到消息,被触发紧紧是浏览器到这里有空余时间,不代表有空余时间执行任务,因此,要计算运行到port2 这里的时候当前时间是多少
  4. 然后利用运行到port2的时间和这一帧结束的时间做比较,如果当前的时间超过了这一帧结束的时间,代表此帧失效,不能用在处理回调任务
  5. 如果没有超过,就用这一帧结束的时间减去执行到此的时间,看看还有多少剩余时间可以执行任务,如果剩余时间大于0,并且有回调任务函数,那就运行回调任务函数吗,并且将timeRemaining:myRequestIdleCallback.tiemRemaing作为其回调任务函数参数的属性

3. fiber构架的实现

前置知识弄明白了,现在就真正进入主题,认识和实现fiber架构

在react中, 每个渲染都有两个阶段,一个是Reconcilation(协调),一个是Commit(提交)

  • 协调阶段:可以简单的任务是diff阶段,这个过程可以被中断(可以被拆成很多小任务块,可以这么认为,一个组件就是一个任务),这个阶段会找出所以的节点变更(例如,节点的新增删除,属性的变更,这些变更在react里面称为副作用)
  • 提交阶段:将上一次计算出的副作用,一次执行,这个阶段必须同步执行,不能被打断

2.1 react子节点存储结构

react的遍历的节点其实就是一个节点树,这个节点树使用的存储结构其实就是使用的 兄弟孩子表示法 的链表式存储,具体如图:
【节点存储示意图】

fiber节点存储示意图.png
只是这个子节点每个都加了一个parent域,方便查找其父亲
因为这是一个典型的树形结构,react在遍历整个虚拟dom树的时候,采取的是先根遍历,例如上面的存储示意图上的结构,先根遍历的顺序就是:根->B->C->C1->B1->B2

(重点复习二叉树,树的遍历如果看的吃力的话)

2.2 react的fiber

  1. 按先先根遍历遍历整个虚拟dom树
  2. 在遍历虚拟dom树的时候,会重头开始建立fiber树,这样每个fiber子树就是就是一个工作单元(一开始的根节点就会包装成fiber树的根节点)
  3. 然后申请每一帧浏览器的可用时间,执行工作单元,如果可用时间消耗完毕就,当前帧没有可用时间该帧就作废,申请下一帧
  4. 执行工作单元,并建立其fiber子树
  5. 建立fiber子树的同时,这时候就相当于是后根遍历顺序,建立副作用域链
  6. 当没有工作单元可以执行的时候,这时候就遍历副作用域链,做相关的副作用操作(节点的增删改)
// 模拟的测试虚拟节点
const VnodeEelemet = {
    type:'div',
    props:{
        id:'A',
        children:[
            {
                type:'div',
                props:{
                    id:'B',
                    children:[
                        {
                            type:'div',
                            props:{
                                id:'C',
                                children:[]
                            }
                        },
                        {
                            type:'div',
                            props:{
                                id:'C1',
                                children:[]
                            }
                        },
                    ]
                }
            },
            {
                type:'div',
                props:{
                    id:'B1'
                }
            },
            {
                type:'div',
                props:{
                    id:'B2'
                }
            }
        ]
    }
}

let root = document.getElementById('root');
// 最开始的工作单元,也就是一个fiber树
let workUnit = {
    stateNode:root, // 此fiber对应的静态节点
    props:{ // fiber的属性
        children:[VnodeEelemet],
        id:'root'
    },
    sibling:null,
    child:null,
    parent:null
}

// 下一个工作单元
let nextUnitWork = workUnit;
function workLoop (deadline){
    while(nextUnitWork &&deadline.timeRemaining()>0){
        // 执行当前工作单元并返回下一个工作单元
        nextUnitWork = preformUnitOfWork(nextUnitWork)
    }
    if(!nextUnitWork){
        commitRoot()
    }
}

function commitRoot(){
    let currentFiber = workUnit.firstEffect;
    console.log(workUnit,'fistEffect')
    while(currentFiber){
        if(currentFiber.effectTag === 'PLACEMENT'){
            currentFiber.parent.stateNode.appendChild(currentFiber.stateNode);
        }
        currentFiber = currentFiber.nextEffect
    }
    workUnit = null
}

function preformUnitOfWork(workingFiber){
    // 1. 创建真实dome
    // 2. 创建fiber子树
    console.log(workingFiber,'')
    beginWork(workingFiber);
    if(workingFiber.child){
        return workingFiber.child
    }
    while(workingFiber){
        // 如果没有儿子,其实当前节点就结束了完成了
        completeUnitWork(workingFiber);
        if(workingFiber.sibling){
            return workingFiber.sibling
        }
        // 指向父亲,方便查询兄弟
        workingFiber = workingFiber.parent
    }
}

function beginWork(workingFiber){
    // console.log( workingFiber)
    console.log('beginWork', workingFiber,workingFiber.props.id)
    // 创建真实节点,但是此时并不挂载
    if(!workingFiber.stateNode){
        workingFiber.stateNode = document.createElement(workingFiber.type)
        Object.keys( workingFiber.props ).forEach( key=>{
            if(key !== 'children'){
                workingFiber.stateNode[key] = workingFiber.props[key]
            }
        })
    }
    //创建子fiber
    let previousFiber;
    if(!workingFiber.props.children){
        return
    }
    workingFiber.props.children.forEach( (child,index)=>{
        let childFiber = {
            type:child.type,
            props:child.props,
            parent:workingFiber,
            effectTag:'PLACEMENT', //插入操作
            nextEffect:null//下一个有副作用的节点
        }
        console.log(child,'xxx')
        if(index === 0){
            workingFiber.child = childFiber
            previousFiber = childFiber
            //console.log(previousFiber,'previousFiber')
        }else{
            //console.log(previousFiber,'previousFiberxxx')
            previousFiber.sibling = childFiber
            previousFiber = childFiber
        }
    })
    // console.log(previousFiber,'end')
}

// 在建立副作用链上,firstEffect作用就是将链头提升,lastEffect就是用来构建整个链表的(构建方式是后根遍历)
function completeUnitWork(workingFiber){
    console.log('completeUnitWork', workingFiber.parent,workingFiber.props.id)
    // 构建副作用链nextEffect !== null(对dom进行插入,更改属性都属于副作用操作)
    const parentFiber = workingFiber.parent;
    if( parentFiber ){
        // 当前fiber有副作用的子链表挂载到父亲身上
        if(!parentFiber.firstEffect){
            // 副作用头节点提升
            // console.log(parentFiber,'提升firstEffect')
            parentFiber.firstEffect = workingFiber.firstEffect
        }
        
        if(workingFiber.lastEffect){
            if(parentFiber.lastEffect){
                parentFiber.lastEffect.nextEffect = workingFiber.firstEffect
            }
            parentFiber.lastEffect = workingFiber.lastEffect
        }
        // 再把自己挂上去(如果自己有副作用的话)
        if(workingFiber.effectTag){
            if(parentFiber.lastEffect){
                parentFiber.lastEffect.nextEffect = workingFiber
            }else{
                parentFiber.firstEffect = workingFiber
            }
            parentFiber.lastEffect = workingFiber
        }

    }
    
    
}
myRequestIdleCallback(workLoop)
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享