Event Loop 是 JavaScript 异步编程的核心思想,也是前端进阶必须跨越的一关。同时,它又是面试的必考点,特别是在 Promise 出现以后,各类各样的面试题层出不穷,花样百出。这篇文章从现实生活中的例子入手,让你完全理解 Event Loop 的原理和机制,并能游刃有余的解决此类面试题。
先来一道面试题镇楼html
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end');`
你是否有见过此类面试题?接下来让咱们一步一步搞懂他!前端
首先明确一点,js是一门单线程语言。也就是说同一时间只能作一件事。
JavaScript为何是单线程?与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?java
因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。
但单线程容易引发阻塞,好比:面试
alert(1); console.log(2); console.log(3); console.log(4);
alert弹框只要不点击肯定那就永远不会打印出2,3,4。
为了防止主线程堵塞,javaScript有了同步和异步的概念。编程
同步:同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步:异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。这也就是定时器并不能精确在指定时间后输出回调函数结果的缘由。promise
具体来讲,异步执行的运行机制以下。(同步执行也是如此,由于它能够被视为没有异步任务的异步执行。)浏览器
(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。数据结构
(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。异步
(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。async
(4)主线程不断重复上面的第三步。
当咱们调用一个方法的时候,JavaScript 会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。这个执行环境中保存着该方法的私有做用域、上层做用域(做用域链)、方法的参数,以及这个做用域中定义的变量和 this 的指向,而当一系列方法被依次调用的时候。因为 JavaScript 是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是所谓执行栈。
"任务队列"是一个事件的队列(也能够理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务能够进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件之外,还包括一些用户产生的事件(好比鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。可是,因为存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
咱们注意到,在异步代码完成后仍有可能要在一旁等待,由于此时程序可能在作其余的事情,等到程序空闲下来才有时间去看哪些异步已经完成了。因此 JavaScript 有一套机制去处理同步和异步操做,那就是事件循环 (Event Loop)。
示意图以下:
以去银行办业务为例,当 5 号窗口柜员处理完当前客户后,开始叫号来接待下一位客户,咱们将每一个客户比做 宏任务
,接待下一位客户
的过程也就是让下一个 宏任务
进入到执行栈。
因此该窗口全部的客户都被放入了一个 任务队列
中。任务队列中的都是 已经完成的异步操做的
,而不是注册一个异步任务就会被放在这个任务队列中(它会被放到 Task Table 中)。就像在银行中排号,若是叫到你的时候你不在,那么你当前的号牌就做废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来之后还须要从新取号。
在执行宏任务时,是能够穿插一些微任务进去。好比你大爷在办完业务以后,顺便问了下柜员:“最近 P2P 暴雷很严重啊,有没有其余稳妥的投资方式”。柜员暗爽:“又有傻子上钩了”,而后叽里咕噜说了一堆。
咱们分析一下这个过程,虽然大爷已经办完正常的业务,但又咨询了一下理财信息,这时候柜员确定不能说:“您再上后边取个号去,从新排队”。因此只要是柜员可以处理的,都会在响应下一个宏任务以前来作,咱们能够把这些任务理解成是 微任务
。
大爷听罢,扬起 45 度微笑,说:“我就问问。”
柜员 OS:“艹...”
这个例子就说明了:你大爷永远是你大爷 在当前微任务没有执行完成时,是不会执行下一个宏任务的!
总结一下,异步任务分为 宏任务(macrotask)
与 微任务 (microtask)
。宏任务会进入一个队列,而微任务会进入到另外一个不一样的队列,且微任务要优于宏任务执行。
宏任务:script(总体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)
微任务:Promise.then、 MutaionObserver、process.nextTick (Node.js)
setTimeout(() => { console.log('A'); }, 0); var obj = { func: function() { setTimeout(function() { console.log('B'); }, 0); return new Promise(function(resolve) { console.log('C'); resolve(); }); }, }; obj.func().then(function() { console.log('D'); }); console.log('E');
先把打印结果呈上
再把解释呈上:
setTimeout
放到宏任务队列,此时宏任务队列为 ['A']setTimeout
放到宏任务队列,此时宏任务队列为 ['A', 'B']'C'
then
放到微任务队列,此时微任务队列为 ['D']console.log('E');
,打印出 'E'
'D'
'A'
和 'B'
再来一个?
let p = new Promise(resolve => { resolve(1); Promise.resolve().then(() => console.log(2)); console.log(4); }).then(t => console.log(t)); console.log(3);
打印结果:
Promise.resolve()
的 then() 方法放到微任务队列,此时微任务队列为 ['2']4
p
的 then() 方法放到微任务队列,此时微任务队列为 ['2', '1']3
2
和 1
async/await 仅仅是生成器的语法糖,因此不要怕,只要把它转换成 Promise 的形式便可。下面这段代码是 async/await 函数的经典形式。
async function foo() { // await 前面的代码 await bar(); // await 后面的代码 } async function bar() { // do something... } foo();
其中 await 前面的代码
是同步的,调用此函数时会直接执行;而 await bar();
这句能够被转换成 Promise.resolve(bar())
;await 后面的代码
则会被放到 Promise 的 then() 方法里。所以上面的代码能够被转换成以下形式,这样是否是就很清晰了?
function foo() { // await 前面的代码 Promise.resolve(bar()).then(() => { // await 后面的代码 }); } function bar() { // do something... } foo();
最后咱们回到开篇那个题目
function async1() { console.log('async1 start'); // 2 Promise.resolve(async2()).then(() => { console.log('async1 end'); // 6 }); } function async2() { console.log('async2'); // 3 } console.log('script start'); // 1 setTimeout(function() { console.log('settimeout'); // 8 }, 0); async1(); new Promise(function(resolve) { console.log('promise1'); // 4 resolve(); }).then(function() { console.log('promise2'); // 7 }); console.log('script end'); // 5
script start
settimeout
添加到宏任务队列,此时宏任务队列为 ['settimeout']
async1
,先打印出 async1 start
,又由于 Promise.resolve(async2())
是同步任务,因此打印出 async2
,接着将 async1 end
添加到微任务队列,,此时微任务队列为 ['async1 end']promise1
,将 promise2
添加到微任务队列,,此时微任务队列为 ['async1 end', promise2]
script end
async1 end
和 promise2
settimeout
Node.js 在升级到 11.x 后,Event Loop 运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval 和 setImmediate) 就马上执行微任务队列,这点就跟浏览器端一致。
const p1 = new Promise((resolve, reject) => { console.log('promise1'); resolve(); }) .then(() => { console.log('then11'); new Promise((resolve, reject) => { console.log('promise2'); resolve(); }) .then(() => { console.log('then21'); }) .then(() => { console.log('then23'); }); }) .then(() => { console.log('then12'); }); const p2 = new Promise((resolve, reject) => { console.log('promise3'); resolve(); }).then(() => { console.log('then31'); });
promise1
then11
,promise2
添加到微任务队列,此时微任务队列为 ['then11', 'promise2']
promise3
,将 then31
添加到微任务队列,此时微任务队列为 ['then11', 'promise2', 'then31']
then11
,promise2
,then31
,此时微任务队列为空then21
和 then12
添加到微任务队列,此时微任务队列为 ['then21', 'then12']
(由于then21和then12都是第二层then)then21
,then12
,此时微任务队列为空then23
添加到微任务队列,此时微任务队列为 ['then23']
then23
这道题实际在考察 Promise 的用法,当在 then() 方法中返回一个 Promise,p1 的第二个完成处理函数就会挂在返回的这个 Promise 的 then() 方法下,所以输出顺序以下。
const p1 = new Promise((resolve, reject) => { console.log('promise1'); // 1 resolve(); }) .then(() => { console.log('then11'); // 2 return new Promise((resolve, reject) => { console.log('promise2'); // 3 resolve(); }) .then(() => { console.log('then21'); // 4 }) .then(() => { console.log('then23'); // 5 }); }) .then(() => { console.log('then12'); //6 });
将不断更新完善,欢迎批评指正!
http://www.ruanyifeng.com/blo...
https://juejin.im/post/5cbc0a...