JS异步编程

319 阅读6分钟

一、如何理解JS异步编程的,EventLoop,消息队列都是做什么的,什么是宏任务,什么是微任务?

1. 首先要了解什么是同步,什么是异步:

  举个简单的例子说明一下:假如你下班回家,要做两件事,烧一壶开水去浴室洗澡同步执行这两件事的话就是先烧水,等水烧好了,再去洗澡,异步的话就是先把水烧上,然后在烧水的过程中去洗澡。

  这个例子可能不是很贴切,因为在JS的异步操作中还涉及到回调函数,但是这个例子可以帮你理解异步编程的概念。

2. 为什么JS是单线程的:

  JS原本的任务是负责浏览器和用户之间的交互,因为要操作DOM,所以必须是单线程的,试想如果有多个线程同时操作同一个DOM元素,那浏览器就懵了。但是排队等待依次执行的机制又有很多的弊端,所以JS提供了异步编程的方案。

3. 异步编程方案与回调函数:

  实现异步编程的方案有很多:回调函数,Promise对象,事件监听,发布/订阅等等,其实核心就是回调函数,回调函数是所有异步编程方案的根基,还是烧水和洗澡的例子,我们来完善一下:

  假如我们在烧水之后要把烧好的水倒入保温杯中,那么就需要把这个事告诉你的朋友,那异步流程就变成了先烧水,然后告诉你的朋友等水烧好要倒到杯子里,然后你去洗澡,在这个流程中,把水倒进杯子就是回调函数,也就是在任务完成后你需要执行的操作,你就是异步操作的调用者,你的朋友就是异步操作的执行者,总结起来就是由调用者定义,交给执行者执行的函数就叫回调函数

4. EventLoop事件轮询和消息队列:

  在真正的开发中,会有众多的同步任务和异步任务,那到底要按照什么顺序执行呢,这就牵扯到了事件轮询,简单的说就是一个规范,规定了任务在浏览器中的执行顺序,那这个顺序是什么,稍后再说,我们先要了解栈和队列。

5. 栈和队列:

  栈和队列都可以理解为存放任务的容器,区别在于队列是先进先出,而栈是后进先出,假如队列内有五个任务,是1,2,3,4,5,执行顺序就是1,2,3,4,5,如果是栈内,则是5,4,3,2,1。

  在浏览器中会有一个调用栈(Call Stack)和消息队列(Queue),当代码从上到下执行时,会把任务压入调用栈中执行,执行结束后会将任务弹出

6. 消息队列:

  消息队列是用来存放回调函数的容器,所以也称为回调队列,在调用栈清空时,浏览器开始轮询消息队列,按顺序执行消息队列中的任务。

7. 宏任务与微任务

  常见的宏任务有setTimeout,setInterval,UI交互事件等等,常见的微任务有Promise.then,Object.observe等等。这些其实只要记住就好。

如果非要研究宏任务与微任务的区别,这里提供以下几种思路:

  1. 宏任务是由宿主发起的,比如浏览器或Node(本文不讨论Node,这里是举例);而微任务是由JS引擎发起的
  2. 宏任务会在另一个线程执行,微任务不会(你可能有疑问:JS不是单线程的吗?JS是单线程的没错,但是浏览器不是单线程的,比如setTimeout的倒计时是在浏览器的其他线程单独处理的)
  3. 每次执行栈执行的代码就是一个宏任务,而微任务在当前 task 执行结束后立即执行的任务 这几种思路的方向都不同,如果暂时理解不了可以先记住,在应用中慢慢求证。

8. EventLoop的执行顺序

  终于到了重点了!

  EventLoop称作事件轮询,也叫事件循环,EventLoop的作用只有一个,就是监听调用栈(Call Stack)和消息队列(Queue),一旦调用栈清空,EventLoop就会从消息队列中取出第一个回调函数,压入调用栈中执行。

  我理解的EventLoop(好理解版):代码从上到下执行,如果遇到微任务就放到微任务队列中,遇到宏任务就放到宏任务队列中,所有同步代码执行完成后,先去微任务队列里把所有的微任务都执行完,再去宏任务队列里按顺序执行宏任务,每执行完一个宏任务,就去微任务队列看看有没有产生新的微任务,如果有就执行微任务,没有就执行下一个宏任务,直到所有任务都执行完。

  我上述的EventLoop在做面试题的时候是完全够用的。但是我查了很多资料,也有人认为根本就没有微任务队列,微任务是在当前宏任务执行结束后立即执行的任务,你可以把调用栈中的所有代码看做一个大的宏任务,那么微任务就是在这个大的宏任务执行结束后立即执行的。

  不管用那种方式理解,都可以确定一点:微任务不需要到整个队伍的末尾重新排队等待执行,所以你可以把微任务当做是宏任务的“插曲”,微任务就是在当前宏任务执行结束后,到下个宏任务开始执行前的这段空档中执行的。

小练习:将下面异步代码用Promise的方式改进

setTimeout(function () {
  var a = 'hello';
  setTimeout(function () {
    var b = 'world';
    setTimeout(function () {
      var c = 'I am programmer';
      console.log(a + b + c);
    }, 10)
  }, 10)
}, 10)

总结上述代码:

  1. 因为console.log(a + b + c)引用了a和b,所以闭包会保存这两个变量,代码最终会打印helloworldI am programmer
  2. 每个定时器有10毫秒的延迟

如果没有定时器,只是单纯的传递变量,很容易就能写出:

var p = new Promise(resolve => {
  var a = 'hello';
  resolve(a);
})
p.then(a => {
  var b = 'world';
  return a + b;
}).then (value => {
  var c = 'I am programmer';
  console.log(value + c);
})

但是a, b,c都是在定时器里赋值的,如果单纯的把定时器放在.then中,下一个.then只会获得定时器的返回值,这不是我们想要的,所以在.then中要返回一个Promise对象。我们改造一下上面的代码:

var p = new Promise((resolve) => {
  setTimeout(function () {
    var a = 'hello';
    resolve(a);
  }, 10)
})
p.then(a => {
  return new Promise(resolve => {
    setTimeout(function () {
      var b = 'world';
    resolve(a + b);
    }, 10)
  })
}).then(a => {
  return new Promise(resolve => {
    setTimeout(function () {
      var b = 'I am programmer';
      console.log(a + b);
    }, 10)
  })
})

这样就可以得到同样的效果了。

希望对看文章的人有所帮助,如果有歧义的地方可互相探讨修改~