React Fiber架构 -- requestAnimationFrame和requestIdleCallback (一)

1,399 阅读3分钟

基础

1. 什么是时间片?

时间片.png

如上图所说,同一个cpu永远不可能同时真正运行多个任务,只能看起来像是同时运行。

一段cpu时间,也就是从进程开始运行到被抢占的时间。

例: 你同时输入两篇文档:A.txt和B.txt; 你在A中输入一个字之后,再在B中输入一个字,轮流输入,直至完成。 总的看来你似乎在同时进行两篇文章的录入,你可以说我一边写A一边写B。但是具体到某个字时,就是沿着时间的前进,AB交替进行了。 而你每个字输入所占用的这段时间,我们就可以称之为时间片。

2. 帧率

一帧也就是一个静止的画面,帧率也就是每秒处理图形的次数(主要和显卡有关)。

刷新率(主要和显示器有关)1/60hz 代表 1秒 最多能刷新60次屏幕。刷新率决定了帧率的上限。

如果电脑屏刷新率60hz,也就是最多 1s能刷新60次图像。

换句话说,60hz 差不多一帧需要的时间 1/60 ≈ 16.6ms

上面说的和浏览器什么关系?

浏览器

1. 浏览器一帧

life of frame(一帧),下图为约16.6ms 浏览器在这一帧中要做的事情。

屏幕截图 2022-04-04 084104.png

主要 :

输入事件(用户点击等事件优先级高,因为防止用户操作觉得卡顿有延迟)

=> javaScript timers(定时器)

=> (开始帧)事件 (window resize scroll change media)

=> requestAnimationFrame

=> layout(计算样式+更新布局) => 绘制

=> idle peroid(空闲阶段) idle callback

如果用户调用了 requestldleCallback方法,则会在一帧的时间的末尾执行requestldleCallback里的callback 方法。

!注意 js线程和 渲染线程 是互斥的。

2. requestAnimationFrame

let btn = document.getElementById('btn');
  let onDiv = document.getElementById('progress-bar');
  let start;
  let current;
 

  function progress() {
      onDiv.style.width = onDiv.offsetWidth + 1 + 'px';
      onDiv.innerHTML = onDiv.offsetWidth + '%';
      if (onDiv.offsetWidth < 100) {
          var old = Date.now() -current;
          current = new Date;
          requestAnimationFrame(progress)
      }
  }

  btn.addEventListener('click', function () {
      onDiv.style.width = 0;
      onDiv.innerHTML = '0%'
      progress();
      current = Date.now()
  })

结果

屏幕截图 2022-04-04 085748.png

我的电脑显示1帧大概13,14 ,刷新率高。

屏幕截图 2022-04-04 091741.png

常见面试题 requestAnimationFrame 和setTimeout区别 。不赘述。

requestAnimationFrame用来做动画,跟着帧率走,在计算样式和更新布局前执行,setTimeout在队列的末尾执行。

requestAnimationFrame 和setTimeout 都是宏任务。

3. requestldleCallback

requestldleCallback(callback,{timeout:1000});

(注意! requestIdLeCallback 这个只有chrome浏览器有,React内部自己实现了一个。)

这一帧时间也就是cpu分配给程序的时间,cpu 给浏览器 16.6ms来处理任务。

具体步骤:

requestldleCallback时间片.png

上图在约定时间内完成任务,再归还控制权什么意思?

约定时间: 假如还剩下4ms,约定你callback最好只执行4ms.

归还控制权: 如果你在程序里写了 requestldleCallback 回调,其中callback处理需要20ms,那么他要坚持等待该任务处理完,才能归还cpu占用控制权。

也就是说,如果一帧时间大致 16.6ms,那么如果执行到最后阶段(空闲阶段)还有时间,如果空闲阶段还剩余 2ms,callback方法执行需要20ms,那么浏览器就会卡住 18ms ,直到这个一帧走完,才会走下一帧,归还cpu占用控制权。

例:

  //首先,通过阻塞写了一个sleep 睡眠函数后续需要使用
  function sleep(time) {
      var old = new Date();
      while (true) {
          var now = new Date();
          if (now - old == time) {
              return;
          }
      }
  }
  
 var works = [() => {
      // 这是任务最小单元  无法再切分
      console.log('第一个任务开始');
      // sleep(20);
      console.log('第一个任务结束')
  }, () => {
      console.log('第二个任务开始');
      // sleep(20);
      console.log('第二个任务结束')
  }, () => {
      console.log('第三个任务开始');
      // sleep(20);
      console.log('第三个任务结束');
  }]

  // fiber 是把整个任务 分成若干个很多个小任务,每次执行一个任务。
  // 执行完成后会看看有没有剩余时间,如果有,继续执行下一个任务。如果没有放弃执行,交给浏览器调度。
  // 当你的刷新频率是60Hz  才是 16.6ms

  // 执行第一帧
  window.requestIdleCallback(workLoop, { timeout: 1000 });

  function workLoop(deadline) {
      
      console.log(deadline.timeRemaining(), '-- 剩余时间 --');
      //如果当前的这一帧 有空闲时间  在这里执行works第一个函数操作。
      while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
          perFormUnitOfwork()
      }
      if (works.length > 0) {
          window.requestIdleCallback(workLoop, { timeout: 1000 })
          //timeout 1000 告诉浏览器即使没有空闲时间也得帮我执行,不能超过1s
      }
  }

  function perFormUnitOfwork() {
      works.shift()();
  }

结果:

下一帧.png

当前works没有阻塞任务时,workLoop 只执行了一次,也就是 利用 callback 中返回的参数 deadline判断当前 帧剩余时间有没有,如果有,继续执行下一个任务,没有则继续走下一帧 (走requestIdleCallback方法)。

    var works = [() => {
        // 这是任务最小单元  无法再切分
        console.log('第一个任务开始');
        sleep(20);
        console.log('第一个任务结束')
    }, () => {
        console.log('第二个任务开始');
        sleep(20);
        console.log('第二个任务结束')
    }, () => {
        console.log('第三个任务开始');
        sleep(20);
        console.log('第三个任务结束');
    }]

结果: 下一帧.png

说明works走了三次。 requestIdleCallback执行了三次,走了三个帧。