12

依然是:经济基础决定上层建筑。


说明

  • 首先,旨在搞清常用的同步异步执行机制
  • 其次,暂时不讨论node.js的Event Loop执行机制,以下关于浏览器的Event Loop执行机制
  • 最后,借鉴了很多前辈的研究文章,非常感谢,此文主要是梳理所学,还请保持质疑以追求正确的知识

要点

  • 基本概念
  • 同步异步操作
  • Event Loop

基本概念

先解释现代js引擎几个概念。

runtime

  • stack(栈):这里放着js正在执行的任务。理解事件循环一(浅析)一文有对 stack 的 example 解释。
  • heap(堆):一个用来表示内存中一大片非结构化区域的名字,对象都被分配在这。
  • queue(队列):一个 js runtime 包含了一个任务队列,该队列是由一系列待处理的任务组成。而每个任务都有相对应的函数。当栈为空时,就会从任务队列中取出一个任务,并处理之。当该任务处理完毕后,栈就会再次为空。(queue的特点是先进先出(FIFO))。

为了方便描述与理解,作出以下约定:

  • stack 栈为主线程
  • queue 队列为任务队列(等待调度到主线程执行)

同步异步

js 是一门单线程语言。 js 引擎有一个主线程(main thread)用来解释和执行 js 程序,实际上还存在其他的线程。例如:处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程(例如在node.js中)等等。这些线程可能存在于 js 引擎之内,也可能存在于 js 引擎之外,在此我们不做区分。不妨叫它们工作线程。但是前辈们颇有一种小本本记好的说法,那就是,要相信 js 单线程的本质,其他一切看似多线程,都是纸老虎。哈哈哈哈哈哈哈哈哈哈哈哈哈......

任务分为同步任务(synchronous)和异步任务(asynchronous),如果所有任务都由主线程来处理,会出现主线程被阻塞而使得页面“假死”。为了主线程不被阻塞,异步任务(如:AJAX异步请求,定时器等)就会交给工作线程来处理,异步任务完成后将异步回调函数注册进任务队列,等待主线程空闲时调用。流程如图:

同步异步


// example
console.log('example-start')

setTimeout(() => {
  console.log('setTimeout-0')
}, 0)

console.log('example-end')

/* chrome result
 * 
    example-start
    example-end
    setTimeout-0
 *
 */

上面一个简单的小 js 片段的执行过程:

  • 主线程开始同步任务执行,执行console.log('example-start')
  • 然后接下来,主线程遇见一个异步操作setTimeout,将改异步任务交给工作线程处理,异步任务完成之后,将回调函数注册进任务队列,等待被调用
  • 继续同步任务处理,执行console.log('example-end')
  • 主线程空闲,调用任务队列中等待执行的回调函数,执行console.log('setTimeout-0')

最后借用Philip Roberts的生动形象的一张图,callback queue可以简单理解为任务队列,详细的下面会讲。

chrome

Event Loop

然而Event Loop并没有上面图中描述那么简单。心塞塞 : (

根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。

setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务(回调函数)。来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

仔细查阅规范可知,异步任务可分为 task(部分文章也称为 macro-task) 和 micro-task 两类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

  • task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(node.js 环境)
  • micro-task主要包含:Promise.then、MutaionObserver、MessageChannel、process.nextTick(node.js 环境)

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)
  • 检查是否存在 micro-task,如果存在则不停地执行,直至清空 micro-task queue
  • 更新 render
  • 主线程重复执行上述步骤

Event Loop

一个事件循环(Event Loop)中,主线程从任务队列中取出一个任务 task 执行时,而这个正在执行的任务就是从 task queue(部分文章也称为 macro-task queue)中来的。当这个 task 执行结束后,js 会将 micro-task queue中所有 micro-task 都在同一个 Event Loop 中执行,当这些 micro-task 执行结束后还能继续添加 micro-task 一直到整个 micro-task 队列执行结束。然后当前本轮的 Event Loop 结束,主线程可以继续取下一个 task 执行。所以更详细的 Event Loop 的流程图如下:

Event Loop


// example
console.log('example-start')

setTimeout(() => {
  console.log('setTimeout-0') // setTimeout-1
}, 0)

new Promise((resolve, reject) => {
  console.log('promise-1')
  resolve('promise-2')
  Promise.resolve().then(() => console.log('promise-3')) // then-1
}).then((response) => { // then-2
  console.log(response)
  setTimeout(() => {
    console.log('setTimeout-10') // setTimeout-2
  }, 10)
})

console.log('example-end')

/* chrome result
 * 
    example-start
    promise-1
    example-end
    promise-3
    promise-2
    setTimeout-0
    setTimeout-10
 *
 */

上面一个简单的 js 片段的执行过程:

  • 第一轮事件循环:
    第一轮事件循环
  • 第二轮事件循环:
    第二轮事件循环
  • 第三轮事件循环:
    第三轮事件循环

如果上文理解有误或者有疑惑,欢迎交流。

参考


好记性不如烂笔头。


Huooo
328 声望33 粉丝

Coding happily and friendly and elegantly.