最近琢磨了好久的Javascript的事件循环机制,看了很多国内的博客总觉得写的还是不够深,很多都只说了Javascript的事件分为同步任务和异步任务,遇到同步任务就放在执行栈中执行,而碰到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件。自己对大概的基础有所了解之后也没接着深入去查资料,这就导致我在面试的时候被面试官一点一点深挖的时候就懵了(囧
函数调用栈与任务队列
Javascript有一个main thread 主进程和call-stack(一个调用堆栈),在对一个调用堆栈中的task处理的时候,其他的都要等着。当在执行过程中遇到一些类似于setTimeout等异步操作的时候,会交给浏览器的其他模块(以webkit为例,是webcore模块)进行处理,当到达setTimeout指定的延时执行的时间之后,task(回调函数)会放入到任务队列之中。一般不同的异步任务的回调函数会放入不同的任务队列之中。等到调用栈中所有task执行完毕之后,接着去执行任务队列之中的task(回调函数)。
用Philip Roberts的演讲《Help, I’m stuck in an event-loop》之中的一张图表示就是
在上图中,调用栈中遇到DOM操作、ajax请求以及setTimeout等WebAPIs的时候就会交给浏览器内核的其他模块进行处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。等到这些模块处理完这些操作的时候将回调函数放入任务队列中,之后等栈中的task执行完之后再去执行任务队列之中的回调函数。
从setTimeout看事件循环机制
下面用Philip Roberts的演讲中的一个栗子来说明事件循环机制究竟是怎么执行setTimeout的。
首先main()函数的执行上下文入栈(对执行上下文还不了解的可以看我的上一篇博客)。
代码接着执行,遇到console.log(‘Hi’),此时log(‘Hi’)入栈,console.log方法只是一个webkit内核支持的普通的方法,所以log(‘Hi’)方法立即被执行。此时输出’Hi’。
当遇到setTimeout的时候,执行引擎将其添加到栈中。
调用栈发现setTimeout是之前提到的WebAPIs中的API,因此将其出栈之后将延时执行的函数交给浏览器的timer模块进行处理。
timer模块去处理延时执行的函数,此时执行引擎接着执行将log(‘SJS’)添加到栈中,此时输出’SJS’。
当timer模块中延时方法规定的时间到了之后就将其放入到任务队列之中,此时调用栈中的task已经全部执行完毕。
调用栈中的task执行完毕之后,执行引擎会接着看执行任务队列中是否有需要执行的回调函数。这里的cb函数被执行引擎添加到调用栈中,接着执行里面的代码,输出’there’。等到执行结束之后再出栈。
小结
上面的这一个流程解释了当浏览器遇到setTimeout之后究竟是怎么执行的,相类似的还有前面图中提到的另外的API以及另外一些异步的操作。
总结上文说的,主要就是以下几点:
- 所有的代码都要通过函数调用栈中调用执行。
- 当遇到前文中提到的APIs的时候,会交给浏览器内核的其他模块进行处理。
- 任务队列中存放的是回调函数。
- 等到调用栈中的task执行完之后再回去执行任务队列之中的task。
测试
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
这段代码是我从网上前不久的一篇文章80%应聘者都不及格的 JS 面试题中找到的,现在我们就分析一下这段代码究竟是怎么输出最后文章中所说的最后的执行状态:
40% 的人会描述为:5 -> 5,5,5,5,5,即第 1 个 5 直接输出,1 秒之后,输出 5 个 5;
- 首先i=0时,满足条件,执行栈执行循环体里面的代码,发现是setTimeout,将其出栈之后把延时执行的函数交给Timer模块进行处理。
- 当i=1,2,3,4时,均满足条件,情况和i=0时相同,因此timer模块里面有5个相同的延时执行的函数。
- 当i=5的时候,不满足条件,因此for循环结束,console.log(new Date, i)入栈,此时的i已经变成了5。因此输出5。
- 此时1s已经过去,timer模块将5个回调函数按照注册的顺序返回给任务队列。
- 执行引擎去执行任务队列中的函数,5个function依次入栈执行之后再出栈,此时的i已经变成了5。因此几乎同时输出5个5。
- 因此等待的1s的时间其实只有输出第一个5之后需要等待1s,这1s的时间是timer模块需要等到的规定的1s时间之后才将回调函数交给任务队列。等执行栈执行完毕之后再去执行任务对列中的5个回调函数。这期间是不需要等待1s的。因此输出的状态就是:5 -> 5,5,5,5,5,即第1个 5 直接输出,1s之后,输出 5个5;
问题
看到这里,对事件循环机制有了一个大概的了解了,可是细想,其中还有一些另外值得深入的问题。
下面通过一个栗子来说明:
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
在这段代码里面,多了一个promise,那么我们可以思考下面这个问题:
- promise的task会放在不同的任务队列里面,那么setTimeout的任务队列和promise的任务队列的执行顺序又是怎么的呢?
- 到这里大家看了我说了这么多的task,那么上文中一直提到的task究竟包括了什么?具体是怎么分的?
如果到这里大家还是没太懂的话,那么接下来我会接着深入再细说不同task的事件循环机制。
当然,以上都是我自己鄙陋的见解,欢迎大家批评指正。
参考资料:
JavaScript定时器的工作原理
《Help, I’m stuck in an event-loop》
【转向Javascript系列】从setTimeout说事件循环模型
JavaScript 运行机制详解:再谈Event Loop
本文转自 https://zhuanlan.zhihu.com/p/26229293,如有侵权,请联系删除。
在上一篇文章里面我大致介绍了JavaScript的事件循环机制,但是最后还留下了一段代码和几个问题。
那我们先从这段代码开始看哇
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
在这段代码里面,setTimeout和Promise都被称之为任务源,来自不同任务源的回调函数会被放进不同的任务队列里面。
setTimeout的回调函数被放进setTimeout的任务队列之中。而对于Promise,它的回调函数并不是传进去的executer函数,而是其异步执行的then方法里面的参数,被放进Promise的任务队列之中。也就是说Promise的第一个参数并不会被放进Promise的任务队列之中,而会在当前队列就执行。
其中setTimeout和Promise的任务队列叫做macro-task(宏任务),当然如我们所想,还有micro-task(微任务)。
- macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
- micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver
其中上面的setImmediate和process.nextTick是Node.JS里面的API,浏览器里面并没有,这里就当举例,不必纠结具体是怎么实现的。
事件循环的顺序是从script开始第一次循环,随后全局上下文进入函数调用栈,碰到macro-task就将其交给处理它的模块处理完之后将回调函数放进macro-task的队列之中,碰到micro-task也是将其回调函数放进micro-task的队列之中。直到函数调用栈清空只剩全局执行上下文,然后开始执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次执行macro-task中的一个任务队列,执行完之后再执行所有的micro-task,就这样一直循环。
分析执行过程
下面分析的思路按照波同学之前所写的深入核心,详解事件循环机制中的思路进行分析。
以之前的栗子作为分析的对象,来分析事件循环机制究竟是怎么执行代码的
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
注意下面所有图中的setTimeout任务队和最后的函数调用栈中存放的都是setTimeout的回调函数,并不是整个setTimeout定时器。
1.首先,script任务源先执行,全局上下文入栈。
2.script任务源的代码在执行时遇到setTimeout,作为一个macro-task,将其回调函数放入自己的队列之中。
3.script任务源的代码在执行时遇到Promise实例。Promise构造函数中的第一个参数是在当前任务直接执行不会被放入队列之中,因此此时输出 1 。
4.在for循环里面遇到resolve函数,函数入栈执行之后出栈,此时Promise的状态变成Fulfilled。代码接着执行遇到console.log(2),输出2。
5.接着执行,代码遇到then方法,其回调函数作为micro-task入栈,进入Promise的任务队列之中。
6.代码接着执行,此时遇到console.log(3),输出3。
7.输出3之后第一个宏任务script的代码执行完毕,这时候开始开始执行所有在队列之中的micro-task。then的回调函数入栈执行完毕之后出栈,这时候输出5
8.这时候所有的micro-task执行完毕,第一轮循环结束。第二轮循环从setTimeout的任务队列开始,setTimeout的回调函数入栈执行完毕之后出栈,此时输出4。
总结
总的来说就是:
- 不同的任务会放进不同的任务队列之中。
- 先执行macro-task,等到函数调用栈清空之后再执行所有在队列之中的micro-task。
- 等到所有micro-task执行完之后再从macro-task中的一个任务队列开始执行,就这样一直循环。
- 当有多个macro-task(micro-task)队列时,事件循环的顺序是按上文macro-task(micro-task)的分类中书写的顺序执行的。
测试
说到这里,我们应该都明白了,下面是一个复杂的代码段(改自深入核心,详解事件循环机制),里面有混杂着的micro-task和macro-task,自己画图试试流程哇,然后再用node执行看看输出的顺序是否一致。
console.log('golb1');
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
setTimeout(function() {
console.log('timeout1_timeout1');
process.nextTick(function() {
console.log('timeout1_timeout1_nextTick');
})
setImmediate(function() {
console.log('timeout1_setImmediate1');
})
});
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
以上就是我所理解的事件循环机制,有偏差之处烦请指正。欢迎大家来交流哇,嘻嘻嘻。
参考资料:
Promise规范
Difference between microtask and macrotask within an event loop context
深入核心,详解事件循环机制
从Promise来看JavaScript中的Event Loop、Tasks和Microtasks
本文转自 https://zhuanlan.zhihu.com/p/26238030,如有侵权,请联系删除。
我是这样理解EventLoop的
在这里插入图片描述
一、前言
众所周知,在使用javascript时,经常需要考虑程序中存在异步的情况,如果对异步考虑不周,很容易在开发中出现技术错误和业务错误。作为一名合格的javascript使用者,了解异步的存在和运行机制十分重要且有必要;那么,异步究竟是何方神圣呢?我们不得不提Event Loop:也叫做事件循环,是指浏览器或Node环境的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是实现异步的原理。作为一种单线程语言,javascript本身是没有异步这一说法的,是由其宿主环境提供的
(EventLoop优秀文章网上有很多,这篇文章是自己的整合和理解)。
注意:Event Loop 并不是在 ECMAScript 标准中定义的,而是在 HTML 标准中定义的;
二、Event Loop知识铺垫
javascript
代码运行时,任务被分为两种,宏任务(MacroTask/Task)
和微任务(MircoTask)
;Event Loop
在执行和协调各种任务时也将任务队列分为Task Queue
和MircoTak Queue
分别对应管理宏任务(MacroTask/Task)
和微任务(MircoTask)
;作为队列,Task Queue
和MircoTak Queue
也具备队列特性:先进先出(FIFO—first in first out)
。
1、微任务(MircoTask)
在 HTML 标准中,并没有明确规定 Microtask,但是实际开发中包含以下四种:
- Promise中的
then、catch、finally
(原理参考:【js进阶】手撕Promise,一码一解析 包懂) - MutationObserver(监视 DOM 变动的API,详情参考MDN)
- Object.observe(废弃:监听标准对象的变化)
- Process.nextTick(Node环境,通常也被认为是微任务)
2、宏任务(MacroTask/Task)
基本上,我们将javascript中非微任务(MircoTask)
的所有任务都归为宏任务,比如:
- script中全部代码
- DOM操作
- 用户交互操作
- 所有的网路请求
- 定时器相关的 setTimeout、setInterval 等
- ···
3、javascript runtime
javascript runtime:为 JavaScript 提供一些对象或机制,使它能够与外界交互,是javascript的执行环境。
javascript执行时会创建一个main thread主线程
和call-stack 调用栈(执行栈,遵循后进先出的规则)
,所有的任务都会被放到调用栈/执行栈等待主线程执行
。其运行机制如下:
在这里插入图片描述
- 1)主线程自上而下依次执行所有代码;
- 2)同步任务直接进入到主线程被执行;
- 3)异步任务进入到
Event Table
,当异步任务有结果后,将相对应的回调函数进行注册,放入Event Queue
; - 4)主线程任务执行完空闲下来后,从
Event Queue(FIFO)
中读取任务,放入主线程执行; - 5)放入主线程的
Event Queue
任务继续从第一步开始,如此循环执行;
上述步骤执行过程就是我们所说的事件循环(Event Loop),上图展示了事件循环中的一个完整循环过程。
三、浏览器环境的Event Loop
不同的执行环境中,Event Loop的执行机制是不同的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 实现并提供了 ECMAScript 标准中的所有数据类型、操作符、对象和方法(注意并没有 DOM)。但它们的 Runtime 并不一样:Chrome 提供了 window、DOM,而 Node.js 则是 require、process 等等
。我们在了解浏览器中Event Loop的具体表现前需要先整理同步、异步、微任务、宏任务之间的关系!
1、同步、异步 和 宏任务、微任务
看到这里,可能会有很多疑惑:同步异步很好理解,宏任务微任务上面也进行了分类,但是当他们四个在一起后就感觉很混乱了,冥冥之中觉得同步异步和宏任务微任务有内在联系,但是他们之间有联系吗?又是什么联系呢?网上有的文章说宏任务就是同步的,微任务就是异步的 这种说法明显是错的!
其实我更愿意如此描述:宏任务和微任务是相对而言的,根据代码执时循环的先后,将代码执行分层理解,在每一层(一次)的事件循环中,首先整体代码块看作一个宏任务,宏任务中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是该宏任务层的微任务;宏任务中的同步代码进入主线程中立即执行的,宏任务中的非微任务异步执行代码将作为下一次循环的宏任务时进入调用栈等待执行的;此时,调用栈中等待执行的队列分为两种,优先级较高先执行的本层循环微任务队列(MicroTask Queue),和优先级低的下层循环执行的宏任务队列(MacroTask Queue)!
注意:每一次/层循环,都是首先从宏任务开始,微任务结束;
在这里插入图片描述
2、简单实例分析
上面的描叙相对拗口,结合代码和图片分析理解:
在这里插入图片描述
答案暂时不给出,我们先进行代码分析:这是一个简单而典型的双层循环
的事件循环
执行案例,在这个循环中可以按照以下步骤进行分析:
- 1、首先区分出该层
宏任务
的范围(整个代码); - 2、区分
宏任务
中同步代码
和异步代码
同步代码:console.log('script start');
、console.log('enter promise');
和console.log('script end');
;
异步代码块:setTimeout
和Promise的then
(注意:Promise中只有then、catch、finally的执行需要等到结果,Promise传入的回调函数属于同步执行代码
); - 3、在
异步
中找出同层的微任务
(代码中的Promise的then
)和下层事件循环的宏任务
(代码中的setTimeout
) - 4、
宏任务
的同步代码优先进入主线程
,按照自上而下顺序执行完毕;
输出顺序为:
//同步代码执行输出
script start
enter promise
script end
- 5、当主线程空闲时,执行该层的
微任务
//同层微任务队列代码执行输出
promise then 1
promise then 2
- 6、首层事件循环结束,进入第二层事件循环(
setTimeout
包含的执行代码,只有一个同步代码)
//第二层宏任务队列代码执行输出
setTimeout
综合分析最终得出数据结果为:
//首层宏任务代码执行输出
script start
enter promise
script end
//首层微任务队列代码执行输出
promise then 1
promise then 2
//第二层宏任务队列代码执行输出
setTimeout
3、复杂案例分析
那么,你是否已经了解上述执行过程了呢?如果完全理解上述实例,说明你已经大概知道浏览器中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 环境下的 Event Loop
在Node
环境下,浏览器的EventLoop
机制并不适用,切记不能混为一谈。这里借用网上很多博客上的一句总结(其实我也是真不太懂):**Node**
中的**Event Loop**
是基于libuv实现的:**libuv**
是 **Node**
的新跨平台抽象层,**libuv**
使用异步,事件驱动的编程方式,核心是提供**i/o**
的事件循环和异步回调。l**ibuv**
的**API**
包含有时间,非阻塞的网络,异步文件操作,子进程等等。
1、Event Loop的6阶段
在这里插入图片描述
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队列的任务。**
2、process.nextTick()
在第二节中就了解到,process.nextTick()
属于微任务,但是这里需要重点提及下:
process.nextTick()
虽然它是异步API的一部分,但未在图中显示。因为process.nextTick()
从技术上讲,它不是事件循环的一部分;- 当每个阶段完成后,如果存在 nextTick,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行(
可以理解为微任务中优先级最高的
)
3、实例分析
老规矩,线上代码:
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、
整体代码
可以看做宏任务,同步代码直接进入主线程执行,输出1,7,13
,接着执行同层微任务且nextTick优先执行输出6,8
; - 2、二层中宏任务中只存在
setTimeout
,两个setTimeout代码块依次进入6阶段中的timer阶段
以t1、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')
})
})
- 3、
setTimeout
中的同步代码立即执行输出2,4,9,11
,nextTick
和Pormise.then
进入微任务执行输出3,10,5,12
; - 4、二层中不存在
6阶段中的其他阶段
,循环完毕,最终输出结果为:1->7->13->6->8->2->4->9->11->3->10->5->12
;
4、当堂小考
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 执行完之后执行;
参考借鉴
本文转自 https://www.jianshu.com/p/98f3729c94cc,如有侵权,请联系删除。