依然是:经济基础决定上层建筑。
说明
- 首先,旨在搞清常用的同步异步执行机制
- 其次,暂时不讨论node.js的Event Loop执行机制,以下关于浏览器的Event Loop执行机制
- 最后,借鉴了很多前辈的研究文章,非常感谢,此文主要是梳理所学,还请保持质疑以追求正确的知识
要点
- 基本概念
- 同步异步操作
- Event Loop
基本概念
先解释现代js引擎几个概念。
- 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可以简单理解为任务队列,详细的下面会讲。
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)中,主线程从任务队列中取出一个任务 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 的流程图如下:
// 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 片段的执行过程:
- 第一轮事件循环:
- 第二轮事件循环:
- 第三轮事件循环:
如果上文理解有误或者有疑惑,欢迎交流。
参考
- Philip Roberts: Help, I’m stuck in an event-loop.
- JavaScript 运行机制详解:再谈Event Loop
- 关于JavaScript单线程的一些事
- 从一道题浅说 JavaScript 的事件循环
- Event Loop的规范和实现
- 这一次,彻底弄懂 JavaScript 执行机制
好记性不如烂笔头。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。