Skip to content

Event Loop(事件循环)

JavaScript是单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

NOTE

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

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

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

同步与异步执行顺序

  1. JavaScript将任务分为同步任务和异步任务,同步任务进入主线中中,异步任务首先到Event Table进行回调函数注册。
  2. 当异步任务的触发条件满足,将回调函数从Event Table压入Event Queue中。
  3. 主线程里面的同步任务执行完毕,系统会去Event Queue中读取异步的回调函数。

只要主线程空了,就会去Event Queue读取回调函数,这个过程被称为Event Loop。

常见异步任务

  • DOM事件
  • AJAX请求
  • 定时器setTimeout和setlnterval
  • ES6的Promise

宏任务和微任务

JavaScript除了广义上将任务划分为同步任务和异步任务,还对异步任务进行了更精细的划分。异步任务又进一步分为微任务和宏任务。

宏任务和微任务分别有各自的任务队列Event Queue,即宏任务队列和微任务队列。

macroTasks(宏任务)包括:

  • script(整体代码)
  • setTimeout、setInterval、setImmediate(定时器)
  • I/O操作(如文件读取、网络请求等)
  • AJAX请求
  • history traversal(浏览器历史记录的前进和后退)
  • requestAnimationFrame

microTasks(微任务)包括:

  • Promise.then
  • MutationObserver
  • process.nextTick(Node.js环境)
  • MutationObserver(DOM变动监听)
  • Object.observe(对象观察)

NOTE

微任务通常比宏任务更快地执行,因为它们在当前宏任务执行完毕后立即执行,而不需要等待下一个宏任务。

Event Loop执行过程

了解到宏任务与微任务过后,我们来学习宏任务与微任务的执行顺序。

  1. 代码开始执行,创建一个全局调用栈,script作为宏任务执行
  2. 执行过程过同步任务立即执行,异步任务根据异步任务类型分别注册到微任务队列和宏任务队列
  3. 同步任务执行完毕,查看微任务队列
    • 若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)
    • 若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空

引用

https://www.ruanyifeng.com/blog/2014/10/event-loop.html

https://juejin.cn/post/7020328988715270157