码字,杂谈

真丶深入理解JavaScript异步编程(一):异步

异步的由来与实现

JS 在设计之初就是单线程的,所以本质上并不存在异步编程。在经过不断的进化和改良之后,现在所谓的异步编程也只是利用任务队列来改变事件的触发顺序,从而在效果上达到异步。

一个生活中的例子

好比我们要吃饭,那就要先做饭,假设焖米饭需要 20 分钟,炒个菜需要 10 分钟。

如果我们一步一步来(全部我们自己动手):

  • 1、焖米饭(20 分钟)
  • 2、炒菜(10 分钟)
  • 3、吃饭

很显然,我们需要 30 分钟才可以吃到饭。

如何加快速度呢?我们可以使用电饭锅来焖米饭。那现在就是:

  • 1、焖米饭(电饭锅用时 20 分钟)
  • 2、炒菜(自身用时 10 分钟)
  • 3、吃饭

我们一开始将焖米饭的事情丢给电饭锅去做,我们只需要关心炒菜这个事情了。等电饭锅做好了饭,它会告诉我们。这样一来,我们要吃上饭,只需要等待 20 分钟,缩短了 10 分钟。

由生活到代码的转换

上面这个例子体现了异步带来的好处。我们自身就是主线程,而电饭锅就是一个任务队列的任务,它会自己处理自己的事情,我们并不需要关心,只需要等待结果即可。就像我们请求后台任务,等待返回结果即可,此时我们的主线程不需要等待结果,还可以做其他事情,有了结果再调用一下就可以了。

上张图说明关系:

《真丶深入理解JavaScript异步编程(一):异步》

上例转换代码:

console.log("我要开始做饭了。");

// 焖米饭,饭好了就可以开饭了
setTimeout(() => {
  console.log("米饭焖好了,可以开饭了。");
}, 2000); // 以2秒替代20分钟

// 炒菜,主线程一直在炒菜
console.log("开始炒菜。");

上例的结果:

我要开始做饭了。
开始炒菜。
米饭焖好了,可以开饭了。

异步操作的执行顺序

上例确实是我们想要的结果,但是没有体现出来炒菜的时间。假设现在炒菜用 30 分钟呢?会不会在 20 分钟的时候就告诉我们可以开饭了呢?

这就要说到异步操作的执行顺序了。

在 JS 中,有如下执行顺序规则:

  • 主线程优先级最高
  • 主线程执行完毕之后,轮询微任务队列并执行
  • 最后轮询宏任务队列并执行

这里引入了两个概念:微任务宏任务,我们稍后介绍。

我们对上例中的代码稍加修改:

console.log("我要开始做饭了。");

// 焖米饭,饭好了就可以开饭了
setTimeout(() => {
  console.log("米饭焖好了,可以开饭了。");
}, 2000); // 以2秒替代20分钟

// 炒菜,让它在主线程中运行一段时间,使用循环模拟
console.log("开始炒菜。");
for (let i = 0; i < 10000000; i++) {
  console.log(" ");
}
console.log("菜炒好了。");

运行的结果显示,它并不会影响“米饭焖好了”在最后调用。这也证明了执行顺序是正确的。

宏任务

JS 中的宏任务有以下几种方式:

  • setTimeout
  • setInterval
  • I/O
  • script

当主线程遇到它们时,会创建一个宏任务,并按时间丢到宏任务队列中去。

微任务

JS 中的微任务有以下几种方式:

  • Promise
  • process.nextTick

同样的,主线程遇到它们也会创建一个微任务,并丢到微任务队列中。

事件循环

在同一次事件循环(event loop)中,永远基于上面提到的执行顺序:主线程 > 微任务 > 宏任务。

《真丶深入理解JavaScript异步编程(一):异步》

举个简单的例子

看如下代码深入理解:

console.log("main start");

// 宏任务
setTimeout(() => {
  console.log("setTimeout 1");
});

// 微任务
new Promise((resolve, reject) => {
  console.log("promise 1"); // Promise 中的代码是同步代码
  resolve("resolve"); // 回调
  console.log("promise 2");
}).then( // 接收回调,这里属于
  val => console.log(val),微任务
  err => {}
);

console.log("main end");

代码经过运行,它的执行结果如下:

main start
promise 1
promise 2
main end
resolve
setTimeout 1

这是比较基本的循环。如果在微任务或者宏任务中再添加微任务/宏任务的话,就会按照事件循环的执行顺序,依次调用执行。

复杂一些的例子

网上看到一个比较复杂的题,图解很详细,我就直接贴过来了:

setTimeout(() => console.log("setTimeout1"), 0); //1宏任务
setTimeout(() => {
  //2宏任务
  console.log("setTimeout2");
  Promise.resolve().then(() => {
    console.log("promise3");
    Promise.resolve().then(() => {
      console.log("promise4");
    });
    console.log(5);
  });
  setTimeout(() => console.log("setTimeout4"), 0); //4宏任务
}, 0);
setTimeout(() => console.log("setTimeout3"), 0); //3宏任务
Promise.resolve().then(() => {
  //1微任务
  console.log("promise1");
});

《真丶深入理解JavaScript异步编程(一):异步》

《真丶深入理解JavaScript异步编程(一):异步》

这个例子还是很有特点的,要搞明白了它,就对事件循环没什么难理解的了。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注