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。
3.1、任务队列的类型
任务队列存在两种类型,一种为microtask queue,另一种为macrotask queue。
图中所列出的任务队列均为macrotask queue,而ES6 的 promise[ECMAScript标准]产生的任务队列为microtask queue。
3.2、两者的区别
microtask queue:唯一,整个事件循环当中,仅存在一个;执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;
macrotask queue:不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。
3.3、更完整的事件循环流程
将microtask加入到JS运行机制流程中,则:
step1:主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈;
step2: 主线程遇到异步任务,指给对应的异步进程进行处理(WEB API);
step3: 异步进程处理完毕(Ajax返回、DOM事件处罚、Timer到等),将相应的异步任务推入任务队列;,
step4:主线程查询任务队列,执行microtask queue,将其按序执行,全部执行完毕;
step5:主线程查询任务队列,执行macrotask queue,取队首任务执行,执行完毕;
step6:重复step4、step5。
microtask queue中的所有callback处在同一个事件循环中,而macrotask queue中的callback有自己的事件循环。
简而言之:同步环境执行 -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个) -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个)...
利用microtask queue可以形成一个同步执行的环境,但如果Microtask queue太长,将导致Macrotask任务长时间执行不了,最终导致用户I/O无响应等,所以使用需慎重。
如图:
检验一下:
[code]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') }) })
第一轮事件循环流程分析如下:
- 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
- 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
- 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
- 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
- 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
宏任务Event Queue |
微任务Event Queue |
setTimeout1 |
process1 |
setTimeout2 |
then1 |
- 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
- 我们发现了process1和then1两个微任务。
- 执行process1,输出6。
- 执行then1,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:
- 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
宏任务Event Queue |
微任务Event Queue |
setTimeout2 |
process2 |
then2 |
- 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
- 输出3。
- 输出5。
- 第二轮事件循环结束,第二轮输出2,4,3,5。
- 第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 直接输出9。
- 将process.nextTick()分发到微任务Event Queue中。记为process3。
- 直接执行new Promise,输出11。
- 将then分发到微任务Event Queue中,记为then3。
宏任务Event Queue |
微任务Event Queue |
process3 |
|
then3 |
- 第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
- 输出10。
- 输出12。
- 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
参考资料:
JS 事件循环机制 - 任务队列、web API、JS主线程的相互协同
阅读更多- JavaScript事件循环(Event Loop)机制
- JavaScript运行机制之事件循环(Event Loop)详解
- JavaScript事件循环机制
- 详解JavaScript事件循环机制
- 详解JavaScript中的Event Loop(事件循环)机制
- javascript中事件循环机制
- JavaScript运行机制之事件循环(Event Loop)详解
- 深入理解JavaScript事件循环机制
- 【JavaScript 学习--12】JS深入理解调用栈,事件循环机制,回调队列
- javascript事件循环机制
- 探探javascript事件机制之先混脸熟
- 深入理解JavaScript的闭包特性如何给循环中的对象添加事件(转)
- javascript之-深入事件机制
- javascript 事件冒泡机制
- Javascript事件模型系列(二)事件的捕获-冒泡机制及事件委托机制
- node.js入门 - 5.事件循环机制(event loop)
- Javascript循环绑定事件 web前端开发博客:http://www.css88.com/
- 事件处理程序实现的另一种方法:浏览器的事件监听机制实现“1事件对应n事件处理程序”(Javascript)
- JavaScript的事件处理机制
- 【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)