面试必问的异步顺序问题,用 Performance 轻松理清

异步代码的执行顺序是前端面试必问的面试题,它主要考察对 Event Loop、宏微任务以及它们执行顺序的理解。

这类问题新手比较容易困惑,老手一不注意也容易掉坑里。

那理清这类问题,有没有什么好的办法呢?

还真有,就是通过 Performance 工具。

Performance 是 Chrome DevTools 内置的用来分析代码执行耗时的工具,它会记录每个函数、每个宏微任务的耗时,并且会把超过 50ms 的长任务标红,这样开发者就可以针对性的优化。

比如这就是一个长任务:

fdab65f8d24262366a2cef84515a92f5.png

优化 js 运行性能的目标就是消除它。

但是今天我们不是用它来分析代码性能,而是用它来看宏微任务的执行情况。

我们准备这样一段代码:

<!DOCTYPE html>
<html lang="en">
  <body>
    <script>
      Promise.resolve().then(() => {
        console.log('promise1');
        const timer2 = setTimeout(() => {
          console.log('timer2');
        }, 0);
      });
      const timer1 = setTimeout(() => {
        console.log('timer1');

        Promise.resolve().then(() => {
          console.log('promise2');
        });
      }, 0);

      console.log('start');
    </script>
  </body>
</html>

很常见的 Promise、setTimeout 混合的代码。

不着急分析它的执行顺序,我们用 performance 工具来看下。

用 npx http-server . 跑一个静态服务器

581c40dc6dc4629f9651585951081221.png

然后浏览器访问。

这里要用无痕模式来跑,不然插件啥的也是在同一个 Event Loop 运行,会影响分析结果。

点击 reload,录制几秒:

3b32a7f89189e656ba6af2cb72c0c4d8.gif

Main 的部分就是主线程,向下拖动放大就可以看到代码执行情况也就是 Event Loop 的详情。

你可能首先会注意到这些一个个很细的 task:

81b55ef933eb2d41f1a768b3176fe60c.png

放大可以看到是渲染的 repaint:

2a9afec676ac065470db66324f6b31d7.png

或者是 reflow 和 repaint:

3bb5f8fc83d2f0c48fd337792babdf70.png

也就是重新计算样式和重新渲染的过程,这些是宏任务。

再往前,可以看到 parse html 的那个 task:

35932c5cef76603e2108859b5f664407.png

这就是我们的 js 代码执行的部分了。

还是可以看出是先执行了一些匿名代码,然后再执行的所有微任务,里面还有一些匿名代码。

怎么能让它更清晰一些呢?

首先,现在显示太细了,火焰图的宽细代表了耗时,所以我们可以加点耗时逻辑。

比如这样:

29775b3e8dcc4ecb1ec3321980890621.png

再重新录制一下,就这样了:

921fb5fa2c08ea21226371d279d377c8.png

现在是不是就明显多了。

可以看到是执行了一个匿名函数,然后执行了所有微任务。

之后 timer 触发了一个宏任务,执行了一些匿名函数,然后执行所有微任务。

后面还有一个 timer 的宏任务。

虽然流程是看清了,但是匿名这块还是比较难受。

倒是可以点击查看详情,然后定位到对应的代码:

71d7f9ceb59aef27ef76da596bba9701.gif

但还是不够直观。

那我们就加上一些有名字的函数吧。

就是这样,换成有名字的函数:

60299b76ea0317e5a26dea35e78576b8.png

然后再分析:

c0bd86f72c72f30ed9ac853b7b7b57ea.png

现在是不是就很清楚了。

然后再看下最开始这段代码:

Promise.resolve().then(() => {
    console.log('promise1');
    const timer2 = setTimeout(() => {
      console.log('timer2');
    }, 0);
});
const timer1 = setTimeout(() => {
    console.log('timer1');

    Promise.resolve().then(() => {
      console.log('promise2');
    });
}, 0);

console.log('start');

对照着 performance 分析出来的结果,就很容易理清宏微任务执行顺序了:

先执行同步代码,打印 start。

然后执行 Promise.then 微任务,打印 promise1。

再打印的是 timer1,因为那个 setTimeout 是在之前被放入任务队列的。

执行完 timer1 的宏任务再执行了 promise2 的微任务。

最后执行了 timer2 的宏任务。

这样我们就通过 Performance 看到了真实的宏微任务的划分以及执行顺序。

当然,微任务不只是 Promise.then,比如 MutationOberser 也是:

MutationObserver 是监听 dom 变化的,比如属性、子节点等:

397a898048e1083a32a66a934335fd85.png

试一下:

d71cf7f9e7299400bd02f616a6f66741.png

可以看到,MutationObserver 确实是微任务。

有的同学一直有疑惑,setTimeout 是宏任务我们知道,那 requestAnimationFrame 呢?

把 setTimeout 换成 rAF:

58db3cdfb21bef89f4ce85369834b488.png

可以看到,rAF 也是宏任务:

bc45b2ecc6dfd4f82c4b88ccd16a91b4.png

全部测试代码:

<!DOCTYPE html>
<html lang="en">
  <body>
    <script>
      function calc() {
        let a = 0;
        for (let i = 0; i < 1000000; i++) {
          a += i;
        }
      }

      function printPromise1() {
        console.log('promise1');
        calc();
      }
      function printpromise2() {
        console.log('promise2');
        calc();
      }
      function printtimer1() {
        console.log('timer1');
        calc();
      }
      function printtimer2() {
        console.log('timer2');
        calc();
      }
      function printstart() {
        console.log('start');
        calc();
      }
      function printMutation() {
        console.log('mutation');
        calc();
      }
      Promise.resolve().then(() => {
        printPromise1();
        const timer2 = setTimeout(() => {
          printtimer2();
        }, 0);
      });
      const timer1 = setTimeout(() => {
        printtimer1();

        Promise.resolve().then(() => {
          printpromise2();
        });
      }, 0);
      new MutationObserver(function () {
        printMutation();
      }).observe(document.body, {
        type: 'attribute',
        attributeFilter: ['aaa'],
      });
      document.body.setAttribute('aaa', 'guang');

      printstart();
    </script>
  </body>
</html>

总结

异步代码执行顺序是前端面试必问的问题,这类问题可以通过 Performance 工具来轻松理清。

通过 Performance 工具可以看到代码执行的 Event Loop,包括宏微任务、执行时间等。

可以看到渲染、setTimeout、requestAnimationFrame 等是宏任务。Promise.then、MutationObserver 是微任务。

这些都是我们自己证实过的。

当你对某段代码的宏微任务执行顺序有疑惑,不妨就试试 Performance 工具吧,在分析宏微任务方面可以说是神器了。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

dbe2d566be0ca2b01b78d158f1b48904.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值