乐趣区

『前端干货篇』: 你不知道的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 操作(从网络读取数据)、定时器 … 不得不等着结果出来,再往下执行。如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死,用户体验很差。
这个时候,任务队列就派上用场了。
在 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(44444)
这五句代码是同步代码,会直接进入主线程,依次执行
btn.onclick = function() {
console.log(22222)
}

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

setTimeout(function() {
console.log(33333)
}, 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 机制!

退出移动版