在浏览器的console里写的js 刷新后就没有了_JS -- 事件循环

e0564a88d380be62a68185fe0306cb01.png

我们常说 JavaScript 是单线程执行的,指的其实是一个进程里只有一个主线程。也就是说 JS 引擎在同一时间内只能做一件事情(任务)。任务分为同步任务和异步任务,同步任务立即执行,异步任务满足某些条件执行。

进程是 CPU资源分配的最小单位;线程是 CPU调度的最小单位

举个例子:假设我们打开一个浏览器,在浏览器中开了若干个 tab,在某个 tab 中看资料,在另外一个 tab 中写笔记;这些 tab 无不干扰,对于我们一般人来说,我们在同一时间内只能做一件事,看资料或者写笔记。我们就可以把浏览器比作一个“进程”,tab 比作一个“线程”,我们一般人就是一个“ JS 引擎”,看资料和写笔记分别可以看成是两个“任务”,这就很好地类比了 js 的单线程执行过程。

js 是单线程的,这在一般情况下没什么问题,但当我们去加载一个网页时,假如有一张图片,下载需要30秒钟,我们就需要等30秒后这张图片下载完成之后再去做其他的任务,这显然是不合理的。好在,浏览器提供了一些 js 引擎不具备的特性,DOM API、定时器、HTTP请求等,可以用来实现异步、非阻塞的行为。

在 ES3 及更早的版本中,js 引擎只能用来执行任务。也就是说,当宿主环境(如浏览器或者 node )拿到一段代码后,js 引擎执行代码的内容和次序完全是由宿主环境来决定的。在 ES5 之后,js 引入了 Promise 这个新特性,使得 js 引擎也可以自己发起任务了。我们把 js 引擎发起的任务称为微任务,把宿主环境发起的任务称为宏任务。主要是以下几种:

  • 宏任务:整体代码的 script、setTimeout、setInterval
  • 微任务:Promise.then、MutationObserver、process.nextTick(node)

这里需要注意的是 Promise 的 then 回调才是微任务,而 new Promise 时,传入的函数是立即执行的

js 引擎和宿主环境都能发起任务,而执行任务的只有 js 引擎,那么,js 引擎是按什么样的规则来处理宏任务和微任务的呢?这个次序是由事件循环来安排的。

在讲事件循环之前,先介绍几个概念:执行栈、任务队列。

  • 执行栈是一个存储任务调用的栈结构。当 js 执行时,会把任务压到一个栈的结构之中,后进先出,执行完函数后弹出。
  • 任务队列是一个储存异步任务的队列结构。当 js 引擎遇到异步的任务时,就会把这个任务放到任务队列中;先进先出的特点,执行完后弹出。任务队列分成两种,一种是宏任务队列,储存宏任务,另外一种是微任务队列,储存微任务。

事件循环

在浏览器中,执行 js 的函数(任务),是由 js引擎的“执行栈”负责的;当 js 引擎接收到一段代码时,就会从上到下解析代码,把同步任务按顺序放到执行栈中去执行。如果遇到异步任务,就会把异步任务先放到任务队列中挂起,等到同步任务执行完成后(也就是执行栈被清空了),再从任务队列中取出一个任务,压到执行栈中去执行。等到执行栈再次被清空,js 引擎就会再去检查任务队列中是否有任务等待执行,如果有则继续压入栈中执行,如此往复,直到任务队列被清空。这个过程,就是事件循环。

7a3c66d959ab349f8fb885a88b4c3df8.png
来自 https://github.com/ljianshu/Blog/issues/54

在事件循环中的任务队列有宏任务队列和微任务队列,那么这两个任务队列的执行顺序又是怎么样的呢?

在事件循环中,js 执行栈执行任务的顺序是:

  1. 执行同步任务;
  2. 遇到异步任务就把他们放到对应的队列中,即:宏任务队列和微任务队列;
  3. 继续执行同步任务,如果遇到异步任务执行 2 ,直至同步任务全部执行完(js 执行栈被清空);
  4. 执行并清空微任务队列;在执行微任务的过程中,如果遇到异步任务,应该把它们分别再放入不同的任务队列中;而后依次取出微任务,并执行,直至微任务队列为空。
  5. 执行并清空宏任务队列和微任务队列;逐个执行宏任务,在执行宏任务时,如果遇到异步任务,也是要把它们分别放入不同的任务队列中的;直到这一个宏任务完成后,再去检查微任务队列是否为空,不为空则先执行 4 ,直至宏任务队列和微任务队列都为空。

需要注意的是,宏任务总是一个一个执行的,而微任务总是一队一队执行的,也就是说,宏任务执行完完整的一个任务之后,就需要去检查一下微任务的队列是否有任务未被执行;而微任务是直至把微任务的队列清空了,才会去检查宏任务队列是否为空。

talk is cheep, show me the code

第一个 :一个宏任务和一个微任务

// 一个同步任务
console.log(1)
// 一个宏任务
setTimeout(() => {
  console.log(2)
}, 0)
// 一个微任务
Promise.resolve().then(()=>{
    console.log(3)    
})

按我们上文提到的规则,上述代码的执行顺序应该是:

  1. 执行同步任务:console.log(1)
  2. 遇到宏任务,放到宏任务队列中,继续;
  3. 遇到微任务,放到微任务队列中,继续;
  4. 没有同步任务了,检查微任务队列;
  5. 微任务队列中有 Promise.resolve().then(()=>{ console.log(3) }),执行 console.log(3)微任务队列清空
  6. 检查宏任务队列,有setTimeout(() => {console.log(2)}, 0),执行 console.log(3)一个宏任务执行完毕
  7. 检查微任务队列,无任务;
  8. 检查宏任务队列,无任务;结束。

所以输出的结果是:1、3、2,这个结果和 setTimeout 、Promise 还有同步代码的书写顺序并没有关系

这就是一个完整且详尽的事件循环的思路了,下面会举一些更为复杂的例子,按这个思路,基本上就能得出正确的代码执行顺序了。

第二个 :宏任务和微任务相互嵌套

// 微任务1
Promise.resolve().then(()=>{
  console.log('1')  
  // 微任务1里的宏任务1
  setTimeout(()=>{
    console.log('2')
  },0)
})
// 宏任务2
setTimeout(()=>{
  console.log('3')
  // 宏任务2里的微任务2
  Promise.resolve().then(()=>{
    console.log('4')    
  })
},0)

这个例子相对来说比较复杂,但是按上面的思路来,依然可以获得正确的代码执行顺序:

  1. 遇到微任务1,放入微任务队列;此时的微任务队列为:微任务1;
  2. 遇到宏任务2,放入宏任务队列;此时的宏任务队列为:宏任务2;
  3. 同步代码执行完毕,检查微任务队列;
  4. 取出微任务1,执行 console.log('1'),遇到宏任务1,放入宏任务队列,此时的微任务队列和宏任务队列分别为:
  5. 微任务队列:空
  6. 宏任务队列:宏任务2、宏任务1
  7. 微任务队列为空,执行第一个宏任务2 console.log('3')
  8. 遇到微任务2,放入微任务队列,执行结束;此时的队列为:
  9. 微任务队列:微任务2
  10. 宏任务队列:宏任务1
  11. 执行完一个宏任务后,检查微任务队列,不为空;取出第一个微任务2,执行 console.log('4');清空微任务队列;
  12. 执行宏任务队列的一个宏任务1,执行 console.log('2');两个队列均为空;结束。

所以,上面代码的运行结果就是:1、3、4、2。

这个例子很好地说明了,宏任务是执行完一个,就会去检查微任务队列是否为空,而微任务是直到清空微任务队列,才会切检查宏任务队列是否为空的。

再看个 确认下

// 微任务1
Promise.resolve().then(()=>{
  console.log(1)  
  // 宏任务1
  setTimeout(()=>{
    console.log(2)
  },0)
  // 微任务2
  Promise.resolve().then(()=>{
    console.log(3)    
  })
})
// 宏任务2
setTimeout(()=>{
  console.log(4)
  // 微任务3
  Promise.resolve().then(()=>{
    console.log(5)    
  })
    // 假设这里用的是 new Promise 的写发的话,结果会是怎么样呢
  //new Promise(resolve => {
  //  console.log(5);
  //  resolve();
  //}).then(data => {
  //  console.log(6);
    //})
},0)

这里就不做具体分析了,写下任务队列的变化顺序吧:

  • 微任务队列:空
  • 宏任务队列:空

===

  • 微任务队列:微任务1
  • 宏任务队列:宏任务2

===

  • 微任务队列:微任务2
  • 宏任务队列:宏任务2、宏任务1

===

  • 微任务队列:空
  • 宏任务队列:宏任务2、宏任务1

===

  • 微任务队列:微任务3
  • 宏任务队列:宏任务1

===

  • 微任务队列:空
  • 宏任务队列:宏任务1

===

  • 微任务队列:空
  • 宏任务队列:空

===

所以,上述例子的任务执行顺序就应该是:微任务1、微任务2、宏任务2、微任务3、宏任务1;

输出结果是:1、3、4、5、2;

如果是用 new Promise 的写法的话,输出的结果就是:1、3、4、5、6、2;这正是因为 new Promise 是立即执行的

总结

  • JavaScript 是单线程执行的
  • 任务分为同步任务和异步任务;异步任务可以由宿主环境发起(宏任务)或者由 js 引擎发起(微任务)
  • 宏任务包括:整体代码的 script、setTimeout、setInterval
  • 微任务包括:Promise.then、MutationObserver、process.nextTick(node)
  • 事件循环就是不断检查任务队列是否还有任务,如果有,就放进执行栈中执行,执行栈清空后再检查任务队列是否有任务,如此反复的过程
  • 宏任务队列和微任务队列是跟随代码的执行动态更新的
  • 在事件循环中,执行“一个”宏任务的前提是微任务队列里没有微任务

参考

浏览器与Node的事件循环(Event Loop)有何区别? · Issue #54 · ljianshu/Blog​github.com
0f8a9da926a0feecc053ed50a91064e4.png
极客时间​time.geekbang.org
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值