【JavaScript】利用事件循环机制实现一个时钟

158 阅读3分钟

关键词:执行栈、任务队列、微任务/宏任务

这篇文章利用事件循环机制来实现一个简单的时钟。在实现时钟之前,我们先来回忆一下事件循环有关概念。

众所周知,JavaScript 是单线程语言,但是网络请求这类任务来说,如果也同步执行的话,会导致页面的阻塞,比如 script 标签的 js 阻塞。好在,JavaScript 引擎有事件循环机制,与主线程无关的,耗时较大的任务就放入任务队列,待同步任务执行完后,再按序执行任务队列里所有的异步任务。

1. 什么是事件循环

协议翻译过来大意是:为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用本节所述的event loops。每种代理都有相互独立的 event loop。用户触发DOM事件、页面渲染、网络请求等都是事件循环来处理的。

2. 事件循环的执行顺序

事件循环的执行顺序大体上分为以下几个步骤:

  • 步骤 ①:首先遇到 script 会将内部的程序作为宏任务,并放入执行栈;
  • 步骤 ②:在 ① 执行的过程中,遇到微任务/宏任务就分别放入微任务队列/宏任务队列;
  • 步骤 ③:待执行栈清空后,就检查微任务队列里是否还有微任务,若有就将最老的一个微任务放入执行栈,并继续执行步骤 ①,直到微任务队列清空
  • 步骤 ④:GPU 渲染页面
  • 步骤 ⑤:然后,检查宏任务队列中是否有宏任务,将最老的一个宏任务放入执行栈,并重复步骤 ①。

注意: js 主线程和页面渲染线程是相互阻塞的,就是说执行 js 时,页面是不会渲染的;页面渲染时不会执行 js。这里就会出现一个问题,如果执行栈中有无限循环或者一直产生新的微任务,页面渲染会被阻塞。

下图简洁而直观地表示了微任务/宏任务的执行顺序(来自:winty-面试题:说说事件循环机制(满分答案来了))。 image.png

3. 异步任务包括哪些

异步任务分为宏任务和微任务。

宏任务主要有:<script> 、setTimeout、setInterval、setImmediate、I/O(Ajax)、UI Rendering、DOM事件等;

微任务主要有:Promise(then、catch、finally)、MutationObserver 等。

4. 实现定时器

实现思路是:通过 new Date 拿到当前时间,并替换 DOM 的 textContent。在过程中不能有阻塞 GPU 渲染页面的 js。

如果这里用while来实现,因为 while 循环会一直执行,直到不满足条件。在这个过程中肯定会阻塞页面渲染,所以页面是不会实时展示刷新页面的。

<div class="time"></div>

<script>
  const app = document.querySelector('.time')
  app.textContent = parseTime()

  while (true) {
    app.textContent = parseTime()
  }
</script>

如果在这里利用 GPU 渲染页面先于宏任务执行,就能实现实时展示时间了。这里可以用 setTimeout 替代 while:

<div class="time"></div>

<script>
  const app = document.querySelector('.time')
  app.textContent = parseTime()

  function update() {
    setTimeout(() => {
      time.textContent = parseTime()
      // 递归调用更新函数
      update()
    })
  }
  update()
</script>

我们来分析一下这段程序的执行过程:

  • 步骤①:找到并执行宏任务,这里首先会执行 script 内的同步代码,并将 update 加入执行栈;
  • 步骤②:查询并执行微任务(这里没有微任务);
  • 步骤③:GPU 渲染页面;
  • 步骤④:update 函数体执行,并开启一个立即执行的定时器,定时器的回调函数内获取到经过格式化的当前时间,并替换 time 的 textContent;
  • 步骤⑤:然后又继续调用 update,重复上面的步骤①。

从上面的步骤可以看出:每执行一次定时器的回调函数之前,页面都被渲染了,所以递归调用 setTimeout 可以实现实时刷新页面的效果。

这里还可以用 requestAnimationFrame 替代 setTimeout。setTimeout 是宏任务,若未指定延迟时间,浏览器大约 4ms 执行一次。与 setTimeout 不同的是,requestAnimationFrame 是在浏览器执行渲染程序时才会执行,而且是在页面渲染之前(详细可以参考文末链接2)。浏览器约 17ms 刷新一次页面,因此 requestAnimationFrame 大约 17ms 执行一次。而所以较 setTimeout 性能较好一点。

最终实现的时钟效果如下:

参考链接

  1. 动画:事件循环(Event lop)
  2. 视频:【事件循环】【前端】事件原理讲解,超级硬核,忍不住转载
  3. HTML5 规范
  4. 从event loop规范探究javaScript异步及浏览器更新渲染时机