vlambda博客
学习文章列表

掉坑里出不来系列之JS的Event Loop

本文来简单说一下JavaScript中的事件循环机制,看完还不懂的可以来捶我。


1、关于JavaScript

JavaScript是一门单线程语言,一直以来都没有改变过,以后也不会改变。在最新的HTML5规范中,提出了webworker的多线程概念,但JavaScript是单线程这一核心仍然未变,所谓的多线程都是单线程模拟出来的,所以,一切JavaScript多线程都是纸老虎。


很多人都知道,JavaScript代码是从上到下执行的,所以先来一段代码压压惊:

setTimeout(() => {  console.log('定时器开始啦');});
new Promise((resolve) => { resolve();  console.log('准备执行个for循环');  for(let i = 0; i < 3;){ i++;}}).then(() => { console.log('执行了then函数');});
console.log('代码执行结束');


本着JavaScript代码从上到下执行的理念,自信地写下了执行顺序:

//定时器开始啦//准备执行个for循环//执行了then函数//代码执行结束


自信满满地拿去浏览器输出结果,您猜怎么着?上来就整 这么一出,那可真是盖了帽了我的老北鼻:

正确执行顺序://准备执行个for循环//代码执行结束//执行了then函数//定时器开始啦

说好的从上到下执行呢... ...


带着疑惑,进入本文的正题,JavaScript的事件循环机制。


2、JavaScript事件循环

JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。    ——《MDN》


JavaScript除了广义上的同步任务和异步任务之外,JavaScript对任务还有更精细的定义:

macro-task(宏任务):整体代码script、setTimeout、setInterval
micro-task(微任务):promiseprocess.nextTick

不同类型的任务会进入到对应的队列中,比如setTimeout和setInterval,都属于一个类型的宏任务,所以会进入到同一个Event Queue。


当我们打开网页的时候,网页的渲染过程就是一大堆的同步任务,比如页面骨架和元素的渲染。而像一些AJAX请求资源等,就是属于异步任务。


下面画个图来描述一下eventloop机制(毕加索之手):

掉坑里出不来系列之JS的Event Loop

大概解释一下这个图的意思,就是代码执行,会进入到一个js的执行栈中从上而下地执行,当遇到异步任务的时候,会抽出来注册一个回调函数,等回调函数有了结果之后,会被放入到异步队列中,等待主线程空闲的时候去调用,此时,执行栈并没有停止,正在继续往下执行,当主线程为空的时候,js引擎会自动去查询异步队列中是否有等待被调用的函数,如果有则执行,没有则开始下一个循环。在这里,你可能会问,我咋个知道主线程什么时候为空啊?不要慌,js引擎存在一个monitoring process进程,会自动持续地检查主线程执行栈是否为空。

掉坑里出不来系列之JS的Event Loop


此时,可能有同学就会说:show me the code,碧池

代码来了:

let parmas = {};axios.get('https://mp.weixin.qq.com',params).then(res => {  console.log('请求成功');});console.log('代码执行完了');

上面是一段简易的异步请求伪代码,执行过程大概如下:

·ajax请求进入Event Table,注册回调函数then;

·执行console.log('代码执行完了');

·ajax请求完成,回调函数then进入Event Queue,等待主线程空闲时间去调用;

·主线程从Event Queue读取回调函数then,并执行console.log('请求成功');


3、到这里,你已经对js的执行顺序有了一定的了解,下面说一下万恶的setTimeout:


setTimeout大家对它的印象就是异步、延迟执行,当需要1秒后才执行的时候,通常会这样写:

setTimeout(() => {  console.log('延迟一秒执行');},1000)


渐渐的setTimeout用的地方多了,问题也慢慢暴露出来了,有时候明明写着延迟3秒执行,实际上却被延迟了5秒甚至10秒,why?


举个栗子:

setTimeout(() => {  console.log('定时器里面');},2000);console.log('定时器之外');

根据前面的结论,我们可以知道setTimeout是异步,所以会先执行console.log('定时器之外'),然后再输出console.log('定时器里面')。


去浏览器验证一下结果,正确。下面对这个栗子改造一下:

setTimeout(() => {  task();//伪代码函数},2000);sleep(10000);//伪代码,表示这个函数执行了10秒

根据前面的经验,很快得出结果:先执行sleep()函数,2秒后执行task(),但实际并非如此,而是经过了长达10秒的等待,才会执行task()函数。


这时候我们需要重新理解一下setTimeout了。setTimeout并不是说,你定义了多少秒延迟,就会在那个时间点调用,而是要看主线程的拥堵情况,必须等主线程被清空了,才有可能去调用setTimeout,上面改造后的栗子是这样执行的:

·task()函数进入Event Table并注册,定时器开始;

·执行sleep()函数,很慢,持续十秒,定时器仍在继续;

·2秒时间到了,定时器结束,task()函数进入Event Queue,等待主线程的调用,但此时sleep()函数仍在主线程执行着,所以只好等待;

·十秒后,sleep()终于执行完了,主线程被清空,此时,Event Queue中的task()函数被压入到主线程执行;


此时的setTimeout等待时间远超2秒,因为JavaScript是单线程语言,当主线程被占用的时候,异步队列的任务只能在一边等待,所以才会导致setTimeout的实际延迟远大于定义。


setTimeout冷知识:根据HTML标准,setTimeout最低延迟为4毫秒。


setInterval作为setTimeout的孪生兄弟,两者差不多的。唯一需要注意的一点是,对于setInterval(fn,ms)来说,并不是意味着每过ms毫秒就会执行一次fn函数,而是每过ms毫秒,就会有一个fn函数进入Event Queue。一旦setInterval的回调函数fn的执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔。


4、说说promise(process.nextTick就展开了,属于nodejs的内容):


事件循环的循序,决定了js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环,接着会执行本次循环的所有微任务,执行完毕后再次从宏任务开始循环。


举个栗子:


setTimeout(() => { console.log('定时器执行');});
new Promise((resolve) => {  //resolve(); console.log('promise');}).then(() => { console.log('then');})
console.log('代码执行完毕');

这段代码作为宏任务,进入到主线程执行,下面分析一下每一个步骤:


·首先遇到了setTimeout,属于异步任务,所以会将它回调函数注册后压入到宏任务的异步队列中;

·接下来遇到了Promise,new Promise会立即执行,所以会执行console.log('promise'),然后then函数会被分发到微任务队列;

·然后遇到了console.log('代码执行完毕'),立即输出,第一个宏任务到此结束;

·上面我们提到,执行完宏任务后会去查询当前的微任务并执行,所以这里会查找是否有需要被执行的微任务,找到了then函数,但它并没有被调用到,所以不需要执行(当你把上面代码中的注释打开,此时会执行then这个微任务),至此,一次事件循环结束;

·开始下一轮的循环,先查询宏任务,发现了setTimeout,这里会执行console.log('setTimeout');

·结束。


为了方便查看,这里输出一下上面栗子的结果(分两种情况):

//第一种情况,是resolve被注释的时候promiseconsolesetTimeout
//第二种情况,是resolve放开的时候promiseconsolethensetTimeout


只要记住两点:JavaScript是一门单线程语言;Event Loop是JavaScript的执行机制。



下面附上一段比较复杂的代码,看看你是否真的掌握了js的执行机制:

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') })})


具体分为三轮循环:

第一轮循环输出:1,7,6,8(两个同步的宏任务,两个微任务,一次宏+一次微,代表一轮循环);

第二轮循环输出:2,4,3,5(第一个定时器);

第三轮循环输出:9,11,10,12(第二个定时器);

完整输出顺序为:1,7,6,8,2,4,3,5,9,11,10,12。


具体流程按照文章上面的内容慢慢分析,就可以得到正确的答案了,我就不对这个展开详细的讨论了。深夜码字不易,有错别字的谅解一下~~有错误描述的我就懒得改了~




参考文章:https://juejin.im/post/59e85eebf265da430d571f89#heading-1