初学者也能懂的Event Loop

502

对于初学者来说,在面试或者学习的过程中,几乎都能接触到事件循环 (Event Loop) 这个名词,但是对于一个刚入门的前端工程师来说大部分都不明白事件循环到底是什么东西,以及它的作用是什么。今天这篇文章就是以一段代码和图片的示例来展示一个简单的事件循环的过程,希望看完这篇文章,能够让你对 JavaScript 的事件循环有一个基本的概念。

JavaScript 是一个单线程的非阻塞的脚本语言,那么这句话中的单线程代表的是什么呢?为了方便理解,我用一段伪代码来定义单线程。

one thred === one call stack === one thing at a time

如上面👆的伪代码,单线程就是在调用过程中只有一个主线程来处理问题,只有一个调用栈,并且每次只能处理一件事情。而非阻塞则是指当有异步任务时,主线程会 pending 这个任务,当异步任务处理完毕后,主线程再根据一定的规则去执行相应的回调。

刚刚我们提到了调用栈,异步任务这些名词,对于程序员来说,栈是很容易理解的数据结构,而 JavaScript 引擎在执行代码的过程中,也是一个出栈入栈的过程。 当主线程在执行的过程中,会一一执行同步的代码,主线程执行的过程中,遇到函数时,会压入栈中,并开始执行函数中的语句,而当遇到异步任务时,主线程会将异步任务加入任务队列,被放入任务队列的事件不会立即回调执行,此时主线程会继续执行代码,直至代码被处理完,当代码被处理完后,调用栈会被清空。调用栈清空后,主线程会查看任务队列中是否存在未完成的任务,若是有的话,压入调用栈。主线程会无限重复此过程,形成一个无限循环,而这个循环就叫作事件循环。

用文字讲了一堆干巴巴的过程之后,我通过讲解一段代码的执行过程来帮助大家理解事件循环的过程。

function foo() {
  console.log(1)
  setTimeout(() => console.log(2))
  Promise.resolve(3).then(val => console.log(val))
  console.log(4)
}

foo()

在看接下来的文章之前,请大家先思考一下这段👆代码执行完之后,会输出什么。

接下来我从主线程执行的角度来分析。

1、首先执行这段代码后,代码的末尾调用了 foo 函数,所以主线程会将 foo 函数压入栈中。

而 foo 函数的第一行是同步的输出,此时会将 1 打印在控制台中,如下图👇

1.png

2、当我们执行 foo 函数的第二行时,遇到了 setTimeout,根据我们之前所讲,遇到了异步任务主线程就会将它添加进任务队列中。如下图👇,任务队列中添加了 setTimeout

2.png

3、代码继续执行,到了 foo 函数的第三行,遇到了 Promise,Promise 也是一个异步任务,所以也将它加入任务队列

3.png

4、接下来执行 foo 函数的第四行,是 console.log(4) 。毫无疑问,直接输出,于是此时的控制台中是这样的👇

4.png

5、这时 foo 函数中的代码都被执行完了,所以 foo 函数就会从调用栈中弹出,并且接下来也没有其他代码执行,所以调用栈会清空👇

5.png

6、按照之前所说,主线程在调用栈清空后,会查看任务队列中是否存在异步任务,此时任务队列中有 setTimeout 以及 promise.then,如果你凭着所谓的经验或者直觉感觉 setTimeout 会先执行的话,那么这篇文章就是为你准备的🤣。

这时我们要引入两个新概念,宏任务与微任务。异步任务有两种类型,宏任务与微任务,它们分别会被分配到不同的队列中,所以任务队列还要分为宏任务队列与微任务队列。我在上文统称任务队列是为了快速建立概念,而如果想准确的理解函数执行过程,则一定要区分宏任务、微任务。

所以这里我先罗列一下宏任务与微任务的分类。

微任务:

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick

宏任务:

  • setTimeout
  • setInterval
  • setImmediate
  • MessageChannel
  • requestAnmationFrame
  • I/O
  • UI 交互操作

在列举完宏任务微任务后,我们再细化异步任务的执行过程:调用栈清空后,会检查微任务的队列中是否有任务,如果有的话,压入调用栈,执行微任务,直到微任务为空。再检查宏任务队列是否有任务,若是有则压一个宏任务入栈,执行完成后,再检查微任务队列是否有事件存在,无限循环此过程。

说了这么多,此时你应该明白,由于 promise.then 是一个微任务,所以它会会先于 setTimeout 调用。先输出 3 ,调用栈中的情况如下图👇

6.png

7、微任务队列为空后,查看宏任务队列,发现还有 setTimeout 未执行,于是执行它,输出 2。

7.png

8、到此,任务队列完全情况,也没有代码可以执行了,这段脚本就执行完毕,调用栈完全为空。控制台中输出

1
4
3
2

8.png

这个输出与你之前的分析是否吻合呢?如果是的话,恭喜你你已经很好的掌握了事件循环的流程。如果你回答错了,也希望在看完本文后能帮助你更深刻的理解事件循环。

总结

JavaScript 是一个单线程的非阻塞的脚本语言,单线程是指在调用过程中只有一个主线程来处理所有任务。非阻塞是指当有异步任务时,主线程会挂起 pending 这个任务,当异步任务处理完毕后,主线程再根据一定规则去执行相应的回调

当调用栈中的任务执行完成,调用栈被清空后,会检查微任务的队列是否有任务,如果有的话,压入调用栈中,执行微任务,直到微任务队列为空。再检查宏任务队列是否有任务,若是有则压一个宏任务入栈。执行完成后,再去检查微任务队列是否有事件存在,无限重复此过程,形成一个无限循环,就叫作事件循环