您的位置:首页 > Web前端 > JavaScript

Javascript事件循环机制

2018-07-05 22:52 465 查看

同步任务和异步任务的区别:

同步任务指的是在主线程上排队执行的任务,只有一个任务执行完毕,另前一个任务才会执行

异步任务:不进入主线程而进入任务队列的任务,一旦主任务中的所有同步任务执行完成,系统就会读取任务队列,看看里面有什么事件,从任务队列中拿到主任务中去执行

 

函数调用栈与任务队列

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。

参考资料:

深入浅出Javascript事件循环机制(上)

JS 事件循环机制 - 任务队列、web API、JS主线程的相互协同

这一次,彻底弄懂JavaScript执行机制l

https://www.tuicool.com/articles/FjIBjer

https://www.cnblogs.com/dong-xu/p/7000139.htm

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: