『前端干货篇』: 你不知道的Event Loop

从一道面试题提及

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操做(从网络读取数据)、定时器...不得不等着结果出来,再往下执行。若是让他们和别的任务同样,都老老实实的排队等待执行的话,执行效率会很是的低,甚至致使页面的假死,用户体验不好。 函数

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

在JS中,全部任务能够分红两种。一种是同步任务,另外一种是异步任务。 spa

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

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

值得一提的是,任务队列不止一条。因为异步任务有不少种,好比事件监听类,定时器类,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机制!

相关文章
相关标签/搜索