18

从一道面试题说起

setTimeout(function() {
  console.log(111);
}, 0);   // 这里定时器时间设置为0ms后执行

console.log(222);

相信这道题很多人都看过,结果是先输出222,再输出111
可能新手会犯错,认为定时器设置0毫秒就等于立即就执行,所以先输出111。但其实内部涉及一个很重要的JS运行机制,也就是我们今天的主角——事件轮询(Event Loop)

JS的特点

在聊Event Loop之前,有必要先讲讲JS的一些重要特点

JS的单线程

JS的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JS不能有多个线程呢?

第一,为了提高效率,减少CPU的开销。在多线程中,CPU需要来回切换线程,就会存在线程切换上的开销。

第二,JS最初设计时,是作为浏览器的脚本语言,主要用途是与用户互动,以及操作DOM。这就决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JS同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

JS的异步

说到JS的异步,可能有同学会问啦,JS是单线程的怎么还能异步执行,这不是自相矛盾吗?的确,单线程和异步确实不能同时成为一个语言的特性,所以它本身不可能是异步的。一定是存在一种机制让它能够异步执行,往下看!

任务队列

JS是单线程就意味着,所有任务需要排队,等前一个任务结束,才能执行后一个任务。但前端的某些任务是非常耗时的,例如IO设备(输入输出设备)、Ajax操作(从网络读取数据)、定时器...不得不等着结果出来,再往下执行。如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死,用户体验很差。

这个时候,任务队列就派上用场了。

在JS中,所有任务可以分成两种。一种是同步任务,另一种是异步任务。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程,而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

任务队列中的任务事件,一般有个共性就是存在"回调函数"。所谓"回调函数",就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务时,执行就是对应的回调函数。

值得一提的是,任务队列不止一条。由于异步任务有很多种,比如事件监听类,定时器类,Ajax请求类...所以可以有很多条任务队列

这样说大家可能还不太明白,我画个图解释下

Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件轮询)。
执行流程

(1)所有同步任务都在主线程上执行,形成一个执行栈(每执行一条代码,向栈中压入这条代码)。

(2)主线程之外,还存在一个"任务队列"。存放异步执行的代码,如定时器、事件监听回调函数等,进入等待状态。

(3)一旦主线程中的所有同步任务执行完毕,就会读取"任务队列",看看里面有哪些任务。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步(轮询)。

具体举个例子吧
假如我们有一段代码

var a = 11111
console.log(a)

var btn1 = document.getElementById('btn1')
btn1.onclick = function() {
    console.log(22222)
}

var btn2 = document.getElementById('btn2')
btn2.onclick = function() {
    console.log(33333)
}

setTimeout(function() {
  console.log(44444)
}, 1000)

console.log(55555)

以上代码在JS引擎中其实是这样执行的

var a = 11111
console.log(a)
var btn1 = document.getElementById('btn1')
var btn2 = document.getElementById('btn2')
console.log(55555)

这五句代码是同步代码,会直接进入主线程,依次执行

btn.onclick = function() {
    console.log(22222)
}

btn2.onclick = function() {
    console.log(33333)
}

setTimeout(function() {
  console.log(44444)
}, 1000)

这三块异步代码不会直接进入主线程,而是先在相应的任务队列中注册

当主线程执行完所有同步代码时,就开始不断轮询任务队列是否有任务需要执行,轮询的过程很快。在轮询过程中,要是用户点击了btn1按钮,任务队列会通知主线程,"说我这有异步代码已就绪,需要你来执行"。这时btn1.onclick就从任务队列中弹出,到主线程中执行

同样的,当过了1s时,任务队列会通知定时器需要执行,这时主线程轮询时得到这条"通知",所以就执行定时器中语句

知道这个机制后,我们再回头看看那个面试题

setTimeout(function() {
  console.log(111);
}, 0);   // 这里定时器时间设置为0ms后执行

console.log(222);

这里的console.log(222) 首先在主线程中执行,而定时器则是先在任务队列中注册。当主线程中代码执行完(也就是console.log('222')这条语句执行完后),主线程开始轮询任务队列中的异步代码,由于定时器设置的时间是0ms,所以任务队列会立即通知主线程,可以执行。最后定时器就会到主线程中开始执行。这就是为什么打印的结果先是222,后111。

总结

JS的事件轮询的机制,使任务队列、JS主线程、异步操作之间可以相互协作。这正是JS语言与众不同的运行方式,也因此使它具备了其他语言不具备的优势。
最后感谢大家百忙之中辛苦观看,也希望这篇文章可以帮助屏幕前的你更好的理解JS的Event Loop机制!


酱菜
2.1k 声望674 粉丝