• JavaScript执行时,js引擎和页面渲染引擎在同一个渲染线程,GUI渲染和JavaScript执行两者互斥。
  • 如果js执行任务事件过程,则会影响浏览器的渲染引擎,造成页面卡顿。

    起因

  • 目前主流设备的屏幕刷新率为60次/秒

  • 每秒绘制的帧数(FPS)达到 60 页面显示才流畅;小于这个值时,用户会感觉到卡顿;
  • 每帧的预算时间是16.667ms
  • 每帧的开始包括样式计算、布局、绘制
  • JavaScript执行和页面渲染在同一个渲染进程中,GUI渲染和JavaScript执行两者互斥
  • 如果js任务执行时间过 长,浏览器渲染会被推迟。

浏览器一秒渲染60帧画面,每帧显示时间在16.66ms。浏览器在绘制每帧时经历的过程,如下图:
Frame生命周期.png

rAF【requestAnimationFrame】回调函数

  • requestAnimationFrame回调函数在绘制之前执行。
  • 每帧绘制时都会执行
  • requestAnimationFrame(callback) 的 callback回调函数中的参数timestamp是回调被调用的时间,也就是当前帧的起始时间
  • rafTime 等于 performance.timing.navigationStart + performance.now ; 约等于Date.now();

    1. <body>
    2. <div id="box"
    3. style="background-color: red;width: 1px; height: 20px;color: antiquewhite;"
    4. ></div>
    5. <button>start</button>
    6. <script>
    7. let div = document.querySelector("#box");
    8. let button = document.querySelector("button");
    9. let startTime;
    10. function progress() {
    11. div.style.width = div.offsetWidth + 1 + "px";
    12. div.innerHTML = div.offsetWidth + "%";
    13. if (div.offsetWidth < 100) {
    14. startTime = Date.now();
    15. requestAnimationFrame(progress);
    16. }
    17. }
    18. button.onclick = function () {
    19. div.style.width = 0;
    20. startTime = Date.now();
    21. // 点击按钮时,调用一次
    22. requestAnimationFrame(progress);
    23. };
    24. </script>
    25. </body>

    requestIdleCallback

  • requestIdleCallback 在每帧绘制完成后如果有剩余时间才执行,而不影响用户交互的关键事件,如动画、窗口缩放和输入响应

  • 正常一帧任务完成后,时间小于16.6ms还有剩余时间,就会执行 requestIdleCallback 里面的任务。
  • requestAnimationFrame 的回调在每帧时必定执行,属于高优先级任务;requestIdleCallback 回调则不能保证每帧都执行,属于低优先级任务。

image.png

  1. function sleep(delay) {
  2. for (let now = Date.now(); Date.now() - now < delay; ) {}
  3. }
  4. let allStart = 0;
  5. const works = [
  6. () => {
  7. allStart = Date.now();
  8. console.log("this first work start");
  9. sleep(20);
  10. console.log("this first work end");
  11. },
  12. () => {
  13. console.log("this second work start");
  14. sleep(15);
  15. console.log("this second work end");
  16. },
  17. () => {
  18. console.log("this third work start");
  19. sleep(10);
  20. console.log("this third work end");
  21. console.log(Date.now() - allStart);
  22. },
  23. ];
  24. requestIdleCallback(workLoop, { timeout: 1000 });
  25. // deadline参数是一个对象,有2个属性
  26. // timeRemaining(), 返回此帧还剩下多少时间让用户使用
  27. // didTimeout 判断回调任务callback是否超时
  28. function workLoop(deadline) {
  29. console.log(`this frame remainTime is ${deadline.timeRemaining()}`);
  30. // deadline.timeRemaining() > 0 说明还有剩余时间
  31. // deadline.didTimeout说明任务已经过期,则必须执行
  32. while (
  33. (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
  34. works.length > 0
  35. ) {
  36. performUnitOfWork();
  37. }
  38. if (works.length > 0) {
  39. console.log(`this frame remainTime is ${deadline.timeRemaining()},时间片已经到期,等待下次调度`);
  40. window.requestIdleCallback(workLoop, { timeout: 1000 });
  41. }
  42. }
  43. function performUnitOfWork() {
  44. works.shift()();
  45. }

浏览器事件执行.png

MessageChannel

  • 上面的 requestIdleCallback 方法只有Chrome支持
  • react利用 MessageChannel 模拟 requestIdleCallback ,将回调延迟到绘制操作之后
  • MessageChannel 允许创建一个新的消息通道,并通过它的2个 MessagePort 属性发送数据
  • MessageChannel 创建一个通信管道,这个管道有2个端口,每个端口都可以通过 postMessage 发送数据,另一个端口只要绑定了onmessage回调方法,就可以通过另一个端口传递数据
  • MessageChannel 是宏任务

    1. <script>
    2. let channel = new MessageChannel();
    3. let port1 = channel.port1;
    4. let port2 = channel.port2;
    5. port1.onmessage = function (event) {
    6. console.log("port1接收的数据:", event.data);
    7. };
    8. port2.onmessage = function (event) {
    9. console.log("port2接收的数据:", event.data);
    10. };
    11. port1.postMessage('port1发送数据');
    12. port2.postMessage('port2发送数据');
    13. </script>

    模拟实现 原生的requestIdleCallback

    1. let channel = new MessageChannel();
    2. let activeFrameTime = 1000 / 60;
    3. let frameDeadline; // 每帧的截止时间
    4. let pendingCallback;
    5. let timeRemaining = () => frameDeadline - performance.now();
    6. channel.port2.onmessage = function () {
    7. let currentTime = performance.now();
    8. // 如果帧的截止时间,小于当前时间,说明已经过期
    9. let didTimeout = frameDeadline <= currentTime;
    10. if (didTimeout || timeRemaining() > 0) {
    11. if (pendingCallback) {
    12. // 原生requestIdleCallback回调函数中的2个参数
    13. pendingCallback({ didTimeout, timeRemaining });
    14. }
    15. }
    16. };
    17. window.requestIdleCallback = (callback, options) => {
    18. requestAnimationFrame((rafTime) => {
    19. // rafTime帧的开始时间
    20. console.log("rafTime", rafTime);
    21. frameDeadline = rafTime + performance.now();
    22. pendingCallback = callback;
    23. channel.port1.postMessage("send");
    24. });
    25. };

    Fiber是什么

  • 通过某些调度策略合理分配CPU资源,提高用户的响应速度。

  • 通过 Fiber 架构,react把协调组件数据变化的过程变成可被中断,在适当的时候让出GPU执行权,就可以让浏览器及时处理用户的交互。

Fiber是一个执行单元

  • Fiber是一个执行单元,react在检查现在还剩下多少时间,如果没有时间交出控制权。

image.png

Fiber是一种数据结构

  • react目前的做法使用链表,每个虚拟节点内部表示为Fiber.

image.png

Fiber执行阶段

每个渲染节点有2个阶段:Reconciliation(协调render阶段)和Commit(提交阶段)

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

    render阶段

  • 从顶点开始遍历

  • 如果有儿子,先遍历第一个儿子
  • 如果没有儿子,标志着此节点遍历完成
  • 如果有兄弟节点,继续遍历兄弟节点
  • 如果没有下一个兄弟,返回父节点并标识 父节点遍历完成;如果有叔叔节点,则遍历叔叔节点;没有叔叔节点,则父节点上一个节点遍历结束。整个流程遍历完成

image.png

  1. /*
  2. Fiber是一个执行单元
  3. 1. Fiber是一个执行单元,类似对象。每次执行完一个执行单元,react会检查可使用的时间还有多少,如果没时间就把控制权让出去
  4. 2. 通过Fiber架构,让Reconcilation过程变成可被中断,适时会让出CPU执行权,让浏览器优先处理用户的交互
  5. Fiber执行阶段
  6. 每次渲染包含2个阶段:Reconcilation(协调或render渲染)和Commit(提交阶段)
  7. 协调阶段:Reconcilation可以认为是diff阶段,该阶段可被中断,会造成节点变更,如新增、删除等,这些变更react称为副作用effect
  8. 提交阶段:将上一个阶段计算出来的所有需要处理的副作用一次性执行。这个阶段必须同步执行,不能被打断
  9. */
  10. let A1 = { type: "div", key: "A1" };
  11. let B1 = { type: "div", key: "B1", return: A1 };
  12. let B2 = { type: "div", key: "B2", return: A1 };
  13. let C1 = { type: "div", key: "C1", return: B1 };
  14. let C2 = { type: "div", key: "C2", return: B1 };
  15. A1.child = B1;
  16. B1.sibling = B2;
  17. B1.child = C1;
  18. C1.sibling = C2;
  19. let nextUnitOfWork; // 下一个执行单元
  20. function workLoop() {
  21. while (nextUnitOfWork) {
  22. nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  23. }
  24. if (!nextUnitOfWork) {
  25. console.log("render阶段结束");
  26. }
  27. }
  28. function performUnitOfWork(fiber) {
  29. beginWork(fiber);
  30. if (fiber.child) {
  31. //如果有子,返回第一个子节点 A1之后B1
  32. return fiber.child;
  33. }
  34. while (fiber) { // 如果没有子,说明该节点完成。
  35. completeUnitOfWork(fiber);
  36. // 节点自身完成,开始查看是否有兄弟节点,如果有返回兄弟节点
  37. if (fiber.sibling) {
  38. return fiber.sibling;
  39. }
  40. // 兄弟节点也遍历完毕,则返回父节点,让父节点再次查找兄弟节点,即该节点的叔叔节点。
  41. fiber = fiber.return;
  42. }
  43. }
  44. function completeUnitOfWork(fiber) {
  45. console.log(fiber.key, "end");
  46. }
  47. function beginWork(fiber) {
  48. console.log(fiber.key, "start");
  49. }
  50. nextUnitOfWork = A1;
  51. workLoop();