众所周知,在使用javascript时,常常须要考虑程序中存在异步的状况,若是对异步考虑不周,很容易在开发中出现技术错误和业务错误。做为一名合格的javascript使用者,了解异步的存在和运行机制十分重要且有必要;那么,异步到底是何方神圣呢?咱们不得不提Event Loop:也叫作事件循环,是指浏览器或Node环境的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是实现异步的原理。做为一种单线程语言,javascript自己是没有异步这一说法的,是由其宿主环境提供的
(EventLoop优秀文章网上有不少,这篇文章是本身的整合和理解)。
注意:Event Loop 并非在 ECMAScript 标准中定义的,而是在 HTML 标准中定义的;javascript
javascript
代码运行时,任务被分为两种,宏任务(MacroTask/Task)
和微任务(MircoTask)
;Event Loop
在执行和协调各类任务时也将任务队列分为Task Queue
和MircoTak Queue
分别对应管理宏任务(MacroTask/Task)
和微任务(MircoTask)
;做为队列,Task Queue
和MircoTak Queue
也具有队列特性:先进先出(FIFO—first in first out)
。java
在 HTML 标准中,并无明确规定 Microtask,可是实际开发中包含如下四种:node
then、catch、finally
(原理参考:【js进阶】手撕Promise,一码一解析 包懂) 基本上,咱们将javascript中非微任务(MircoTask)
的全部任务都归为宏任务,好比:编程
javascript runtime:为 JavaScript 提供一些对象或机制,使它可以与外界交互,是javascript的执行环境。
javascript执行时会建立一个main thread主线程
和call-stack 调用栈(执行栈,遵循后进先出的规则)
,全部的任务都会被放到调用栈/执行栈等待主线程执行
。其运行机制以下:
promise
Event Table
,当异步任务有结果后,将相对应的回调函数进行注册,放入Event Queue
;Event Queue(FIFO)
中读取任务,放入主线程执行;Event Queue
任务继续从第一步开始,如此循环执行; 不一样的执行环境中,Event Loop的执行机制是不一样的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 实现并提供了 ECMAScript 标准中的全部数据类型、操做符、对象和方法(注意并无 DOM)。但它们的 Runtime 并不同:Chrome 提供了 window、DOM,而 Node.js 则是 require、process 等等
。咱们在了解浏览器中Event Loop的具体表现前须要先整理同步、异步、微任务、宏任务之间的关系!浏览器
看到这里,可能会有不少疑惑:同步异步很好理解,宏任务微任务上面也进行了分类,可是当他们四个在一块儿后就感受很混乱了,冥冥之中以为同步异步和宏任务微任务有内在联系,可是他们之间有联系吗?又是什么联系呢?网上有的文章说宏任务就是同步的,微任务就是异步的 这种说法明显是错的!
其实我更愿意如此描述:宏任务和微任务是相对而言的,根据代码执时循环的前后,将代码执行分层理解,在每一层(一次)的事件循环中,首先总体代码块看做一个宏任务,宏任务中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是该宏任务层的微任务;宏任务中的同步代码进入主线程中当即执行的,宏任务中的非微任务异步执行代码将做为下一次循环的宏任务时进入调用栈等待执行的;此时,调用栈中等待执行的队列分为两种,优先级较高先执行的本层循环微任务队列(MicroTask Queue),和优先级低的下层循环执行的宏任务队列(MacroTask Queue)!
注意:每一次/层循环,都是首先从宏任务开始,微任务结束;
网络
上面的描叙相对拗口,结合代码和图片分析理解:异步
答案暂时不给出,咱们先进行代码分析:这是一个简单而典型的双层循环
的事件循环
执行案例,在这个循环中能够按照如下步骤进行分析:socket
宏任务
的范围(整个代码);宏任务
中同步代码
和异步代码
console.log('script start');
、console.log('enter promise');
和console.log('script end');
;setTimeout
和Promise的then
(注意:Promise中只有then、catch、finally的执行须要等到结果,Promise传入的回调函数属于同步执行代码
);异步
中找出同层的微任务
(代码中的Promise的then
)和下层事件循环的宏任务
(代码中的setTimeout
)宏任务
的同步代码优先进入主线程
,按照自上而下顺序执行完毕;//同步代码执行输出 script start enter promise script end
微任务
//同层微任务队列代码执行输出 promise then 1 promise then 2
setTimeout
包含的执行代码,只有一个同步代码)//第二层宏任务队列代码执行输出 setTimeout
综合分析最终得出数据结果为:ide
//首层宏任务代码执行输出 script start enter promise script end //首层微任务队列代码执行输出 promise then 1 promise then 2 //第二层宏任务队列代码执行输出 setTimeout
那么,你是否已经了解上述执行过程了呢?若是彻底理解上述实例,说明你已经大概知道浏览器中Event Loop的执行机制,可是,要想知道本身是否是彻底明白,不妨对于下列多循环的事件循环进行分析检验,给出你的结果:
console.log('1'); setTimeout(function() { console.log('2'); new Promise(function(resolve) { console.log('3'); resolve(); }).then(function() { console.log('4') }) setTimeout(function() { console.log('5'); new Promise(function(resolve) { console.log('6'); resolve(); }).then(function() { console.log('7') }) }) console.log('14'); }) new Promise(function(resolve) { console.log('8'); resolve(); }).then(function() { console.log('9') }) setTimeout(function() { console.log('10'); new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) console.log('13')
分析:以下图草稿所示,左上角标a为宏任务队列,左上角标i为微任务队列
,同一层循环中,本层宏任务先执行,再执行微任务;本层宏任务中的非微任务异步代码块做为下层循环的宏任务进入下次循环,如此循环执行;
若是你的与下面的结果一致,恭喜你浏览器环境的Event Loop
你已经彻底掌握,那么请开始下面的学习:
1->8->13->9->2->3->14->4->10->11->12->5->6->7
在Node
环境下,浏览器的EventLoop
机制并不适用,切记不能混为一谈。这里借用网上不少博客上的一句总结(其实我也是真不太懂):Node
中的Event Loop
是基于libuv实现的:libuv
是 Node
的新跨平台抽象层,libuv
使用异步,事件驱动的编程方式,核心是提供i/o
的事件循环和异步回调。libuv
的API
包含有时间,非阻塞的网络,异步文件操做,子进程等等。
Node的Event loop一共分为6个阶段
,每一个细节具体以下:
timers:
执行setTimeout和setInterval中到期的callback。pending callback:
上一轮循环中少数的callback会放在这一阶段执行。idle, prepare:
仅在内部使用。poll:
最重要的阶段,执行pending callback,在适当的状况下回阻塞在这个阶段。check:
执行setImmediate的callback。close callbacks:
执行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。process.nextTick()
重点:如上图所,在Node.js中,一次宏任务能够认为是包含上述6个阶段、微任务microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
在第二节中就了解到,process.nextTick()
属于微任务,可是这里须要重点说起下:
process.nextTick()
虽然它是异步API的一部分,但未在图中显示。由于process.nextTick()
从技术上讲,它不是事件循环的一部分;能够理解为微任务中优先级最高的
)老规矩,线上代码:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) console.log('13')
将代码的执行分区进行解释
分析:以下图草稿所示,左上角标a为宏任务队列,左上角标i为微任务队列
,左上角标t为timers阶段队列
,左上角标p为nextTick队列
同一层循环中,本层宏任务先执行,再执行微任务;本层宏任务中的非微任务异步代码块做为下层循环的宏任务进入下次循环,如此循环执行:
总体代码
能够看作宏任务,同步代码直接进入主线程执行,输出1,7,13
,接着执行同层微任务且nextTick优先执行输出6,8
;setTimeout
,两个setTimeout代码块依次进入6阶段中的timer阶段
以t一、t2
进入队列;代码等价于:setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
setTimeout
中的同步代码当即执行输出2,4,9,11
,nextTick
和Pormise.then
进入微任务执行输出3,10,5,12
;6阶段中的其余阶段
,循环完毕,最终输出结果为:1->7->13->6->8->2->4->9->11->3->10->5->12
;console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') setTimeout(function() { console.log('6'); process.nextTick(function() { console.log('7'); }) new Promise(function(resolve) { console.log('8'); resolve(); }).then(function() { console.log('9') }) }) }) }) process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') setTimeout(function() { console.log('13'); process.nextTick(function() { console.log('14'); }) new Promise(function(resolve) { console.log('15'); resolve(); }).then(function() { console.log('16') }) }) }) setTimeout(function() { console.log('17'); process.nextTick(function() { console.log('18'); }) new Promise(function(resolve) { console.log('19'); resolve(); }).then(function() { console.log('20') }) }) console.log('21')
浏览器
和Node
环境下,microtask 任务队列
的执行时机不一样:Node 端,microtask 在事件循环的各个阶段之间执行;浏览器端,microtask 在事件循环的 macrotask 执行完以后执行;