文章首次发表在 我的博客
最近面试了不少家公司,这道题几乎是必被问到的一道题。以前总以为本身了解得差很少,可是当第一次被问到的时候,殊不知道该从哪里开始提及,涉及到的知识点不少。因而花时间整理了一下。并不只仅是由于面试遇到了,而是理解JavaScript事件循环机制会让咱们日常遇到的疑惑也获得解答。html
通常面试官会这么问,出道题,让你说出打印结果。而后会问分别说说浏览器的node的事件循环,区别是什么,什么是宏任务和微任务,为何要有这两种任务...html5
本篇文章参考了不少文章,同时加上本身的理解,若是有问题但愿你们指出。node
浏览器的事件循环git
node环境下的事件循环github
单线程:web
JavaScript的主要用途是与用户互动,以及操做DOM。若是它是多线程的会有不少复杂的问题要处理,好比有两个线程同时操做DOM,一个线程删除了当前的DOM节点,一个线程是要操做当前的DOM阶段,最后以哪一个线程的操做为准?为了不这种,因此JS是单线程的。即便H5提出了web worker标准,它有不少限制,受主线程控制,是主线程的子线程。面试
非阻塞:经过 event loop 实现。vim
为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲 《Help, I'm stuck in an event-loop》)promise
执行栈: 同步代码的执行,按照顺序添加到执行栈中浏览器
function a() { b(); console.log('a'); } function b() { console.log('b') } a();
咱们能够经过使用 Loupe(Loupe是一种可视化工具,能够帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响)工具来了解上面代码的执行状况。
a()
先入栈a()
中先执行函数 b()
函数b()
入栈b()
, console.log('b')
入栈b
, console.log('b')
出栈b()
执行完成,出栈console.log('a')
入栈,执行,输出 a
, 出栈事件队列: 异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其余任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会马上执行起回调,而是等待当前执行栈中全部任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,若是有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,而后执行其中的同步代码。
咱们再上面代码的基础上添加异步事件,
function a() { b(); console.log('a'); } function b() { console.log('b') setTimeout(function() { console.log('c'); }, 2000) } a();
此时的执行过程以下
咱们同时再加上点击事件看一下运行的过程
$.on('button', 'click', function onClick() { setTimeout(function timer() { console.log('You clicked the button!'); }, 2000); }); console.log("Hi!"); setTimeout(function timeout() { console.log("Click the button!"); }, 5000); console.log("Welcome to loupe.");
简单用下面的图进行一下总结
为何要引入微任务,只有一种类型的任务不行么?
页面渲染事件,各类IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,咱们不能准确地控制这些事件被添加到任务队列中的位置。可是这个时候忽然有高优先级的任务须要尽快执行,那么一种类型的任务就不合适了,因此引入了微任务队列。
不一样的异步任务被分为:宏任务和微任务
宏任务:
微任务:
异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。
在当前执行栈为空时,主线程会查看微任务队列是否有事件存在
当前执行栈执行完毕后时会马上处理全部微任务队列中的事件,而后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务以前执行。
在事件循环中,每进行一次循环操做称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤以下:
简单总结一下执行的顺序:
执行宏任务,而后执行该宏任务产生的微任务,若微任务在执行过程当中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
深刻理解js事件循环机制(浏览器篇) 这边文章中有个特别形象的动画,你们能够看着理解一下。
console.log('start') setTimeout(function() { console.log('setTimeout') }, 0) Promise.resolve().then(function() { console.log('promise1') }).then(function() { console.log('promise2') }) console.log('end')
start
console.log('end')
,输出 end
promise1
, promise回调函数默认返回 undefined, promise状态变成 fulfilled ,触发接下来的 then回调,继续压入 microtask队列,此时产生了新的微任务,会接着把当前的微任务队列执行完,此时执行第二个 promise.then回调,输出 promise2
setTimeout
最后的执行结果以下
表现出的状态与浏览器大体相同。不一样的是 node 中有一套本身的模型。node 中事件循环的实现依赖 libuv 引擎。Node的事件循环存在几个阶段。
若是是node10及其以前版本,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask队列中的任务。
node版本更新到11以后,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就马上执行微任务队列,跟浏览器趋于一致。下面例子中的代码是按照最新的去进行分析的。
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<──connections─── │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
node中事件循环的顺序
外部输入数据 --> 轮询阶段(poll) --> 检查阶段(check) --> 关闭事件回调阶段(close callback) --> 定时器检查阶段(timer) --> I/O 事件回调阶段(I/O callbacks) --> 闲置阶段(idle, prepare) --> 轮询阶段...
这些阶段大体的功能以下:
poll:
这个阶段是轮询时间,用于等待还未返回的 I/O 事件,好比服务器的回应、用户移动鼠标等等。
这个阶段的时间会比较长。若是没有其余异步任务要处理(好比到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
check:
该阶段执行setImmediate()的回调函数。
close:
该阶段执行关闭请求的回调函数,好比socket.on('close', ...)。
timer阶段:
这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否知足定时器的条件。若是知足就执行回调函数,不然就离开这个阶段。
I/O callback阶段:
除了如下的回调函数,其余都在这个阶段执行:
宏任务:
微任务:
Promise.nextTick
process.nextTick 是一个独立于 eventLoop 的任务队列。
在每个 eventLoop 阶段完成后会去检查 nextTick 队列,若是里面有任务,会让这部分任务优先于微任务执行。
是全部异步任务中最快执行的。
setTimeout:
setTimeout()方法是定义一个回调,而且但愿这个回调在咱们所指定的时间间隔后第一时间去执行。
setImmediate:
setImmediate()方法从意义上将是马上执行的意思,可是实际上它倒是在一个固定的阶段才会执行回调,即poll阶段以后。
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');
先执行宏任务(当前代码块也算是宏任务),而后执行当前宏任务产生的微任务,而后接着执行宏任务
script start
async1 start
, 而后执行 async2(), 输出 async2
,把 async2() 后面的代码 console.log('async1 end')
放到微任务队列中promise1
,把 .then()放到微任务队列中;注意Promise自己是同步的当即执行函数,.then是异步执行函数script end
。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码async1 end
、 promise2
, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout
最后的执行结果以下
console.log('start'); setTimeout(() => { console.log('children2'); Promise.resolve().then(() => { console.log('children3'); }) }, 0); new Promise(function(resolve, reject) { console.log('children4'); setTimeout(function() { console.log('children5'); resolve('children6') }, 0) }).then((res) => { console.log('children7'); setTimeout(() => { console.log(res); }, 0) })
这道题跟上面题目不一样之处在于,执行代码会产生不少个宏任务,每一个宏任务中又会产生微任务
start
children4
, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,由于 resolve是放到 setTimeout中执行的children2
,此时,会把 Promise.resolve().then
放到微任务队列中。children3
;而后开始执行宏任务②,即第二个 setTimeout,输出 children5
,此时将.then放到微任务队列中。children7
,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6
;最后的执行结果以下
const p = function() { return new Promise((resolve, reject) => { const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1) }, 0) resolve(2) }) p1.then((res) => { console.log(res); }) console.log(3); resolve(4); }) } p().then((res) => { console.log(res); }) console.log('end');
p1.then
会先放到微任务队列中,接着往下执行,输出 3
p().then
会先放到微任务队列中,接着往下执行,输出 end
p1.then
,输出 2
, 接着执行p().then
, 输出 4
resolve(1)
,可是此时 p1.then
已经执行完成,此时 1
不会输出。最后的执行结果以下
你能够将上述代码中的 resolve(2)
注释掉, 此时 1才会输出,输出结果为 3 end 4 1
。
const p = function() { return new Promise((resolve, reject) => { const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1) }, 0) }) p1.then((res) => { console.log(res); }) console.log(3); resolve(4); }) } p().then((res) => { console.log(res); }) console.log('end');
最后强烈推荐几个很是好的讲解 event loop 的视频: