JavaScript 中的事件循环(event loop),以及微任务 和宏任务的概念

说起事件循环(event loop)之前先要知道的几个问题:

1.js是单线程的。

2.浏览器是多线程的。多个线程相互配合以保持同步,浏览器的线程有:

(1).JavaScript引擎线程,用于解析JavaScript代码。

(2).GUI渲染线程,它与javaScript线程互斥。

(3).事件线程(onclick,onchange...)

(4).定时器线程(setTimeout,setInterval)

(5).异步http线程(ajax)负责数据请求

(6).EventLoop轮询处理线程,事件被触发时该线程会把事件添加到待处理队列的队尾

 来一个灵魂三问:Js为什么是单线程的?为什么需要异步?单线程又是如何实现异步的?

(1).Js为什么是单线程的?

设想一下如果JS是多线程的,现在有两个线程同时对同一个dom进行操作,一个删除该dom,一个修改该dom,

若同时下达这个两个命令,浏览器该如何执行?这样想Js为什么被设计成单线程就应该容易理解了。

(2).Js为什么需要异步?

如果js中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被堵塞。

对于用户而言,堵塞就意味着卡死,用户体验极差。所以Js中存在异步。

(3).Js单线程又是如何实现异步的呢?

通过事件循环(event loop)实现异步。 理解了事件循环(event loop)机制,就理解了js引擎的执行机制。

JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

 

浏览器的多线程中,主线程和异步线程之间是怎么配合的:

主线程发起一个异步请求(比如http请求),相应的工作线程接收请求

并告知主线程已收到通知(异步函数返回);主线程可以继续执行后面的代码,

同时工作线程执行异步任务,工作线程完成工作后,通知主线程;

主线程收到通知后,执行一定的动作(调用回调函数)。

 

JavaScript 的事件循环(event loop)

由于js是单线程的,那么所有的任务就需要排队执行。

javaScript中的任务可以被划分为宏任务(Macrotask)或者微任务(Microtask)

常见的宏任务有:鼠标事件,键盘事件,ajax, setTimeout ,setInterval, 主线程的整体代码(script标签)也是一个宏任务

常见的微任务: process.nextTick, Promise.then(), MutaionOberver

 

简单概括一下事件循环:

1.执行宏任务队列中的第一个任务,执行完后移除它

2.执行所有的微任务,执行完后移除它们

3.执行下一轮宏任务(重复步骤2)

如此循环就形成了event loop ,其中,每轮执行一个宏任务和所有的微任务

 举几个例子练习一下:

console.log(‘1’);

setTimeout(function(){
    console.log(‘2’)
},10);

new Promise(function(resolve){
    console.log(‘3’)
    for( var i=100000 ; i>0 ; i-- ){
        i==1 && resolve()
    }
    console.log(‘4’)
}).then(function(){
    console.log(‘5’)
}).then(function(){ 
console.log(‘6’) 
})

console.log(‘7’);

打印出来的结果是:1 3 4 7 5 6 2

我们分析一下整个过程:

1. 首先执行主线程这个宏任务,从上到下执行,遇到console.log(‘1’) 打印1出来

2. 遇到setTimeout,把它丢给定时器线程处理,然后继续往下执行,并不会阻塞10毫秒,

而此处定时器线程会在,主线程执行完后的10毫秒,把回调函数放入宏任务队列。

3. 遇到new Promise,直接执行,先打印 ‘3‘ 出来,然后执行for循环,达到条件之后,

把promise的状态改为resolved,继续执行打印 ‘4’ 出来

4.遇到promise 的 then, 属于微任务,则把回调函数放入微任务队列

5.又遇到promise 的 then, 属于微任务,则把回调函数放入微任务队列

6. 遇到console.log('7') 打印 ‘7’ 出来

7. 宏任务执行完后会执行所有待执行的微任务,所以会相继打印 ‘5’, ‘6’ 出来。

 至此第一轮循环已经结束了,第一轮循环里的宏任务和微任务都会被移除出任务队列,接下来开启第二轮循环

1.首先查找是否有宏任务,由于setTimeout 的回调被放入了宏任务队列,这里会执行回调函数的代码,打印了 ‘2’ 出来

2. 接着查找是否有微任务,发现没有微任务,则本轮循环结束

 

接下来会重复上面的步骤,这就是event loop 了。后续当我们触发点击事件,有回调函数的话,回调函数也会被放入宏任务队列,

一旦队列里重新有了任务,就会被执行。

 

再来一个例子:

console.log(‘1’);

setTimeout(function(){
    new Promise(function(resolve){
    console.log('promise in setTimeout1');
    resolve();
    }).then(function(){
        console.log('then in setTimeout1');
    })
},10);

new Promise(function(resolve){
    console.log(‘3’);
    for( var i=100000 ; i>0 ; i-- ){
        i==1 && resolve();
    }
    console.log(‘4’)
}).then(function(){
    console.log(‘5’);
});

setTimeout(function(){
    console.log('setTimeout2');
},10);

console.log(‘7’);

结果如下:

 可以发现,第二个setTimeout 的回调函数,执行的比第一个setTimeout里面的promise.then()的回调要晚,

这是因为每次循环只执行一个宏任务,但是却会执行所有待执行的微任务,而第二个setTimeout在宏任务队列的位置在第一个setTimeout后面。

 

 单独说一下定时器:setTimeout()

setTimeout(function(){
    console.log("执行了")
},1000

这段代码我们一般说:1秒后会执行setTimeout里面的函数,但是正确的解释应该是:1秒后setTimeout里面的函数会被推入event queue(事件队列),而事件队列里面的任务,只有在主线程空闲时才会执行。所以只有满足2个条件:一.1秒后 。 二.主线程空闲。才会执行setTimeout里面的回调函数。

需要注意的是,setTimeout()只是将事件插入了"事件队列",如果主线程执行内容很多耗时很长,那只有等主线程执行完成后才能去执行setTimeout指定的回调函数

参考文章:

https://www.cnblogs.com/daisygogogo/p/10116694.html

https://segmentfault.com/a/1190000012806637?utm_source=tag-newest

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

https://zhuanlan.zhihu.com/p/33127885

https://zhuanlan.zhihu.com/p/33136054

https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context

posted @ 2020-07-31 11:00  棠樾  阅读(517)  评论(0)    收藏  举报