【前端面试题】JS事件循环机制(event loop)之宏任务/微任务

258 阅读4分钟

前言

在正式学习JS事件循环(Event loop)之前,我们需要掌握一些基础的浏览器和JS线程的知识。

我们都知道,我们写完的JS代码最终是被打包到浏览器上去执行的,那么浏览器做的事情,只有执行代码吗?答案当然是否定的,浏览器要做的事情很多。他要去解析JS,要去渲染页面,要运行第三方插件等等。由此可见浏览器是多进程的。

浏览器进程

  • Browser进程
  • CPU进程
  • 第三方插件进程

这里面跟我们今天要讲的内容有关系的就是CPU进程。

一个进程里面可以有多个线程。根据前端特性可以将CPU进程分为:

  • GUI线程(用于渲染页面)
  • JS线程(用于执行JavaScript代码)
  • 事件触发线程(管理任务队列,和EventLoop密切相关)
  • 定时触发器线程
  • 异步HTTP请求线程

事件循环(Event loop)

简单理解事件循环就是先确定事件的执行规则,然后按照这个规则循环执行。此时会涉及到宏任务与微任务的概念,后面着重讲解,先说事件循环。

JS是单线程执行的,就好比打电话,必须要打完当前这个电话挂断之后才能继续接打下一个,同理JS在执行代码任务时也是这样一个接一个的执行的。

但是如果一个任务的执行时间很长,它后面的所有任务岂不是就会卡住了嘛。就会造成一个问题,假如我想浏览一个新闻,这个新闻页面里包含了超高清的视频,难道我们的网页要等到这个视频完全加载完成之后再显示出来吗?

当然不是,相信大家一定见过,一个页面文字先出,图片后出来的情况。究其原因,就是将任务分成了两种类型:

当我们打开网站的时候,网页的渲染过程就是一大堆同步任务的,比如页面元素的渲染。而像加载图片、视频这种资源大、耗时久的动作就是异步任务。下面我就来弄清楚执行机制是什么?

先看一段代码:

console.log('script start');
setTimeout(function() { 
    console.log('setTimeout');
}, 0); 
Promise.resolve().then(function() { 
    console.log('promise1'); 
}).then(function() { 
    console.log('promise2'); 
}); 
console.log('script end');

正确的打印顺序是:script start, script end, promise1, promise2, setTimeout。

相信很多人看到这已经开始迷惑了,为什么是这样的呢?

  • 如下导图(此图从网站下载)

image.png

解释一下:

  • 任务和异步任务会分别进入不同的执行空间,同步任务进入主线程优先执行,异步的进入Event table,并注册该异步动作的回调函数。
  • 当异步动作中指定的事情完成之后(比如发送请求给服务端),Event Table会将这个函数移入Event Queue(事件队列)
  • 主线程内的任务执行完成之后,会有一段空置时间(很短),这个时候会去看Event Queue(事件队列)中找函数,如果有Event Queue中有需要执行的函数,那么就会把这个函数放入主线程进行执行。
  • 以上的过程不断重复,形成了我们常说的Event loop(事件循环) 看代码:
let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');复制代码

上面是一段简易的ajax请求代码:

  • ajax进入Event Table,注册回调函数success。
  • 执行console.log('代码执行结束')。
  • ajax事件完成,回调函数success进入Event Queue。
  • 主线程从Event Queue读取回调函数success并执行。

相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。

微任务(Microtasks)、宏任务(task)?

微任务和宏任务皆为异步任务,它们都属于一个队列,主要区别在于他们的执行顺序,Event Loop的走向和取值。那么他们之间到底有什么区别呢?

image.png

由此可见,JS线程在执行代码时正确的顺序是:同步任务--> 微任务--> 宏任务。 一个方法执行时,会向执行栈中加入这个方法的执行环境(也就是整个函数的代码),在这个执行环境里面调用其他方法或者是自己时,对于JS线程而言,无非是再添加一个执行环境而已。这个动作可以一直循环到栈溢出,也就是达到了内存最大值。

宏任务一般指:整体代码script、setTimeout、setInterval、setImmediate。 微任务一般指:原生Promise、process.nextTick、MutationObserver。

其他更加详细的解释可以参考文章: JS事件循环机制(event loop)之宏任务/微任务