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

异步调度机制——Javascript事件循环(Event Loop)机制解析

2019-02-23 22:40 447 查看
版权声明:欢迎转载,转载请注明原始出处 https://blog.csdn.net/xiaomingelv/article/details/87896630

我们都知道,javascript是一个单线程的语言,这就意味着,js在同一个时间片上,只能执行一个任务。只有当前一个任务执行完成之后,才能执行下一个任务。那么当出现网络请求,IO操作这类耗时操作时,由于当前任务什么时候完成我们是不知道的,这时候如果选择等待当前任务完成之后再进行下一步很明显是不明智的,这样会导致性能的严重下降,于是,js的任务类型分为同步和异步两种。

同步任务就不需要多说了,按顺序执行。那么异步任务的执行是怎样进行的呢,这就是本文要解析的主要内容。

首先我们来看一段非常常见的代码

[code]setTimeout(()=>{
console.log('hello world')
},100)

很多新手看到这个操作之后的第一个反应就是:100毫秒之后执行一个输出。如果JavaScript支持多线程,那么上面的操作还有可能在100毫秒之后准确的做出输出。但问题是JavaScript是一个单线程语言,这就决定了它只能处理完当前正在执行的任务之后,才能进行下一个任务,因此是不可能保证在100毫秒之后准确进行输出。所以对于这句代码的正确理解应该是这样的:计时100毫秒之后,如果当前其他任务执行完了,开始执行输出。执行到这段代码时,浏览器会维持一个消息队列,当计时结束时,把当前要执行的操作放入到消息队列的尾部,js引擎在空闲的时候,会按照先进先出顺序,将里面的操作逐个执行。所以,任务需要等待的时间,取决于队列里待处理的消息数量。需要注意的是,上面的这个100毫秒的操作,事实上是由另一个单独的线程来执行的,那么,说好了的单线程呢,怎么又多出一条线程了?我们来看一下下面这幅图

JavaScript虽然是单线程的,但是浏览器是多线程的,我们写的js代码,是在js引擎线程中执行的,而计时操作,则是在定时触发器线程中执行的,而我们平时用得最多的ajax异步操作,发出http请求时,也是由另一条单独的http请求线程来完成的。那么,上面的操作就可以理解为:定时触发器线程计数100毫秒之后,将输出操作放入到消息队列中,当主操作执行之后,js会按顺序执行整个消息队列中的内容。其他的异步操作同理,http请求完成之后同样是将回调操作放入到消息队列中等待执行。那么问题又来了,js是用什么算法来管理这些消息队列的呢?这就引出了这篇文章的主题:事件循环机制

一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。在事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息,直到队列被清空为止,而之所以称其为事件循环,是因为它大概是以以下方式来实现的

[code]while (queue.waitForMessage()) {
queue.processNextMessage();
}

如果当前没有任何消息,

queue.waitForMessage()
 会同步地等待消息到达。

到了这一步,我们对于异步操作的理解大致可以整理为:当执行到异步操作时,浏览器会把需要异步回调的函数维护在一个消息队列中,当主操作执行完之后,会利用事件循环机制遍历消息队列,批量处理异步操作

如果一切就是这么简单的话,那么这篇文章到此就应该结束了,但是,当我们看到了下面这段代码的时候,可能就会有点懵逼

[code]console.log('主流程开始');
setTimeout(function() {
console.log('计时器');
})
var promise = new Promise(function(resolve, reject) {
console.log('promise');
resolve();
})
promise.then(function() {
console.log('promise回调');
})
console.log('主流程结束');

仔细观察一下,如果按照我们刚刚的分析,两个异步操作按顺序添加到消息队列中,首先执行主流程,然后事件循环会遍历上面的队列,那么上面的输出应该是:主流程开始,promise,主流程结束,计时器,promise回调。然而实际执行结果并不是这样,我们把上面的代码运行起来之后会发现正确的输出是:主流程开始,promise,主流程结束,promise回调,计时器。这又是为什么呢?

事实上,事件循环将异步任务分为宏任务和微任务两种,两者的定义如下如下:

微任务 microtask(jobs): 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,例如promise/ajax/Object.observe。

宏任务 macrotask(task):当前调用栈中执行的代码成为宏任务,例如 setTimout/script/IO/UIRendering)。

他们的执行关系如下图所示

宏任务和微任务分属两个不同的消息队列,并且宏任务的执行优先程度要高于微任务,每一次事件循环都会先把宏任务执行完之后,再批量执行微任务。我们再来看看上面的代码,我们从主任务进入消息队列,主进程作为宏任务开始,输出主流程开始,之后setTimeout计时操作开始,计时结束之后将回调任务放到下一次宏任务的消息队列(非本次宏任务),之后执行输出promise,输出主流程结束,到这里,本次宏任务结束,由于promise里面的操作执行结束,回调操作被添加到微任务消息队列中,于是清空微任务队列,输出promise回调,进入下一次事件循环,最后,输出计时器

 

 

小结:

所有的异步回调操作,真正由js负责执行的任务,事实上都是同步执行的,只是以消息队列的形式改变了执行顺序,由事件循环机制负责调度这些任务的执行顺序,宏任务和微任务分处两个不同的消息队列,宏任务的优先级要高于微任务。

参考资料:

并发模型与事件循环

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