从零开始认识Fiber

  • 大多数设备屏幕的刷新频率为60次/秒
  • 页面帧数(FPS)小于60时,会感觉到页面卡顿
  • 每帧的时间大约是16.66毫秒
  • 每帧的包括:样式计算、布局、绘制
  • JavaScript引擎和页面渲染引擎在同一个渲染线程,GUI渲染和JavaScript执行两者是互斥的
  • 任务执行时间过长,浏览器会推迟渲染
    一个帧:
    在这里插入图片描述

Fiber

  • 把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行。
  • 通过Fiber架构,让自己的协调过程变成可被中断的。适时让出CPU执行权,让浏览器及时响应

什么是fiber

  • Fiber是一个执行单元
    在这里插入图片描述
    每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去
  • Fiber是一种数据结构

使用链表,每个虚拟dom节点,内部表示为一个Fiber,可以用一个JS对象来表示

const fiber = {
  stateNode, // 节点实例
  child,     // 子节点
  sibling,   // 兄弟节点
  return,    // 父节点
}

举个🌰

let element= <div id="A1">
      <div id="B1">
        <div id="C1"></div>
        <div id="C2"></div>
      </div>
      <div id="B2"></div>
    </div>

在这里插入图片描述

requestAnimationFrame(每帧执行)

requestAnimationFrame(callback)会在浏览器每次重绘前执行callback回调
🌰

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div style="background-color: yellow;width:0;height: 30px;"></div>
    <button>开始</button>
    <script>
        let div = document.querySelector('div')
        let button = document.querySelector('button')
        let startTime
      
        button.onclick = function () {
            div.style.width = 0
            startTime = Date.now()
            requestAnimationFrame(progress)
        }
        function progress () {
            div.style.width = div.offsetWidth + 1 + 'px'
            div.innerHTML = div.offsetWidth + '%'
            if (div.offsetWidth < 100) {
                console.log(Date.now() - startTime + 'ms')
                startTime = Date.now()
                requestAnimationFrame(progress)
            }
        }
    </script>
</body>
</html>

requestIdleCallback(请求空闲调用)

  • requestIdleCallback在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
  • 正常帧任务完成后没超过16ms,说明时间有富余,此时会执行requestIdleCallback里注册的任务
  • requestAnimationFrame 的回调会在每一帧确定执行,属于高优先级任务
    requestIdleCallback的回调不一定执行,属于低优先级任务

在这里插入图片描述

MessageChannel

  • requestIdleCallback目前只有Chrome支持
  • React利用MessageChannel模拟requestIdleCallback将回调延迟到操作之后执行
  • MessageChannel通过两个MessagePort属性发送数据
  • MessageChannel是一个宏任务
    🌰
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        function progress () {
            console.log('每次帧执行')
            requestAnimationFrame(progress)
        }
        // requestAnimationFrame(progress)
        let channel = new MessageChannel()
        let activeFrameTime = 1000/60  // 16.6
        let frameDeadline // 这一帧的截止时间
        let pendingCallback
        let timeRemaining = () => frameDeadline - performance.now();
        channel.port2.onmessage = function () {
            let currentTime = performance.now()
            // 如果帧的截止时间已经小于当期时间,说明已经过期了
            let didTimeout = frameDeadline <= currentTime
            if (didTimeout || timeRemaining() > 0) {
                if (pendingCallback) {
                    pendingCallback({didTimeout,timeRemaining})
                }
            }
        }

        window.requestIdleCallback = (callback,options) => {
            requestAnimationFrame((rafTime)=>{
                // 每一帧开始的时间加上16.6 就是一帧截止的时间
                frameDeadline = rafTime + activeFrameTime
                pendingCallback = callback
                channel.port1.postMessage('hello')
            })
        }
        function sleep(duration) { // 睡眠时间
            let start = Date.now()
            while (start + duration > Date.now()) {

            }
        }
        const works = [
            () => {
                console.log('A1开始')
                sleep(20)
                console.log('A1结束')
            },
            () => {
                console.log('A2开始')
                sleep(20)
                console.log('A2结束')
            },
            () => {
                console.log('A3开始')
                sleep(20)
                console.log('A3结束')
            },
        ]
        // 让浏览器在空闲的时间执行任务,但是如果已经过期了(超过1秒还没执行),就立刻执行
        requestIdleCallback(workLoop, {timeout: 1000})
        // 循环执行队列
        function workLoop(deadline) {
            console.log('本帧的剩余时间', deadline.timeRemaining())
            // 如果说还有剩余时间,或者此任务已过期了,并且还有没完成的任务
            while((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0 ) {
                performUnitOfWork()
            }
            // 时间片已经到期了,等待下次调度
            if (works.length > 0) {
                requestIdleCallback(workLoop)
            }
        }
        function performUnitOfWork () {
            let work = works.shift();  // 执行任务数组的第一个
            work()
        }

    </script>
</body>
</html>

Fiber执行

每次渲染有两个阶段:Reconciliation(协调render阶段)和Commit(提交阶段)

  • 协调阶段,可以认为是diff阶段,这个阶段可以被中断,这个阶段会找出所有节点变更,例如:节点新增、删除、属性变更等,这些变更被react称为副作用(effects)
  • 提交阶段:将上一个阶段计算出来需要处理的副作用(effects)一次性执行,必须同步执行,不能被打断

render阶段

在这里插入图片描述

  • 从顶点开始遍历
  • 如果有第一个子节点,先遍历第一个子节点如果没有子节点,标志着此节点遍历完成
  • 如果有兄弟节点,遍历兄弟节点,如果没有下一个兄弟节点,返回父节点,此父节点遍历完成
  • 遵循深度优先遍历规则

commit阶段

在这里插入图片描述
firstEffect指向第一个有副作用的子节点,lastEffect指向最后一个有副作用的子节点
🌰 fiber渲染实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id='root'></div>
    <script>
        let root = {
            type: 'div',
            props: {
                id: 'A1',
                children: [
                {
                    type: 'div',
                    props: {
                        id: 'B1',
                        children: [
                        {
                            type: 'div',
                            props: {
                                id: 'C1',
                                children: []
                            },
                            
                        },
                        {
                            type: 'div',
                            props: {
                                id: 'C2',
                                children: []
                            },
                        }
                    ]
                    },
                    
                },
                {
                    type: 'div',
                    props: {
                        id: 'B2',
                        children: []
                    },
                }
            ]
            },
            
        }
        let container = document.getElementById('root')
        const PLACEMENT = 'PLACEMENT' // 插入
        //应用的根,fiber其实也是一个普通的js对象
        let workInProgressRoot = {
            stateNode: container,// 此fiber对应的DOM节点
            props:{children:[root]}// fiber的属性
            // child,
            // sibling,
            // return
        }
         //下一个工作单元
        let nextUnitOfWork = workInProgressRoot

        function workLoop(deadline) {
            // 如果有当前的工作单元,就执行,并返回一个工作单元
            while(nextUnitOfWork) {
                nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
            }
            if (!nextUnitOfWork) {
                commitRoot() // 挂载
            }
        }

        function commitRoot () {
            let currentFiber = workInProgressRoot.firstEffect
            while(currentFiber) {
                if (currentFiber.effectTag === 'PLACEMENT') { // 如果是插入,就挂载
                    currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
                }
                currentFiber = currentFiber.nextEffect
            }
            workInProgressRoot = null
        }

        /**
         * beginWork 1.创建此fiber的真实dom,通过虚拟DOM创建fiber结构
         * @param {*} workingInProgressFiber
         * 
         * **/
        function performUnitOfWork (workingInProgressFiber) {
            // 1.创建真实DOM,并没有挂载,2.创建fiber子树
            beginWork(workingInProgressFiber)
            if (workingInProgressFiber.child) {
                return workingInProgressFiber.child //如果有子节点,返回子节点
            }
            while(workingInProgressFiber) {  // 没有子节点的时候
                completeUnitWork(workingInProgressFiber) // 如果没有子节点,当前节点就结束完成了
                if(workingInProgressFiber.sibling) { // 有兄弟节点,返回兄弟节点
                    return workingInProgressFiber.sibling
                }
                workingInProgressFiber = workingInProgressFiber.return // 没有兄弟节点,指向父亲节点
            }
        }

        function beginWork(workingInProgressFiber) {
            console.log('begin' ,workingInProgressFiber.props.id)
            if (!workingInProgressFiber.stateNode) {
                workingInProgressFiber.stateNode = document.createElement(workingInProgressFiber.type)
            }
            for (let key in workingInProgressFiber.props) {
                workingInProgressFiber.stateNode[key] = workingInProgressFiber.props[key]
            }
            // 在beginWork中不会挂载
            // 创建子fiber
            let previousFiber
            // children是一个虚拟DOM数组

            workingInProgressFiber.props.children.forEach((child, index) => {
                let childFiber = {
                    type: child.type,
                    props: child.props,
                    return: workingInProgressFiber,
                    effectTag: 'PLACEMENT', // 这个fiber对应的dom节点,需要被插入到页面中父DOM里去
                }

                if (index === 0) {
                    workingInProgressFiber.child = childFiber
                } else {
                    previousFiber.sibling = childFiber
                }
                previousFiber = childFiber

            });
        }

        function completeUnitWork (workingInprogressFiber) {
            console.log('completeUnitWork', workingInprogressFiber.props.id)
            // 构建副作用链,effectList 指那些有副作用的节点
            let returnFiber = workingInprogressFiber.return
            if (returnFiber) {
                //把当前fiber的有副作用的子链表挂载到父节点上
                if(!returnFiber.firstEffect) {
                    returnFiber.firstEffect = workingInprogressFiber.firstEffect
                }
                if (workingInprogressFiber.lastEffect) {
                    if (returnFiber.lastEffect) {
                        returnFiber.lastEffect.nextEffect = workingInprogressFiber.firstEffect
                    }
                    returnFiber.lastEffect = workingInprogressFiber.lastEffect
                }
                // 再把自己挂到后面
                if(workingInprogressFiber.effectTag) {
                    if(returnFiber.lastEffect) {
                        returnFiber.lastEffect.nextEffect = workingInprogressFiber
                    } else {
                        returnFiber.firstEffect = workingInprogressFiber
                    }
                    returnFiber.lastEffect = workingInprogressFiber
                }
            }

        }
        // 浏览器空闲时执行
        requestIdleCallback(workLoop)
    
    </script>
</body>
</html>

执行效果:
在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值