Event Loop - JS执行机制

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


说明

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

要点

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

基本概念

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

runtime

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

为了方便描述与理解,做出如下约定:html5

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

同步异步

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

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

同步异步

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

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

console.log('example-end')

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

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

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

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

chrome

Event Loop

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

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

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

仔细查阅规范可知,异步任务可分为 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 片断的执行过程:

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

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

参考


好记性不如烂笔头。

相关文章
相关标签/搜索