原文地址: https://www.jeremyjone.com/766,转载请注明
异步的由来与实现
JS 在设计之初就是单线程的,所以本质上并不存在异步编程。在经过不断的进化和改良之后,现在所谓的异步编程也只是利用任务队列来改变事件的触发顺序,从而在效果上达到异步。
一个生活中的例子
好比我们要吃饭,那就要先做饭,假设焖米饭需要 20 分钟,炒个菜需要 10 分钟。
如果我们一步一步来(全部我们自己动手):
- 1、焖米饭(20 分钟)
- 2、炒菜(10 分钟)
- 3、吃饭
很显然,我们需要 30 分钟才可以吃到饭。
如何加快速度呢?我们可以使用电饭锅来焖米饭。那现在就是:
- 1、焖米饭(电饭锅用时 20 分钟)
- 2、炒菜(自身用时 10 分钟)
- 3、吃饭
我们一开始将焖米饭的事情丢给电饭锅去做,我们只需要关心炒菜这个事情了。等电饭锅做好了饭,它会告诉我们。这样一来,我们要吃上饭,只需要等待 20 分钟,缩短了 10 分钟。
由生活到代码的转换
上面这个例子体现了异步带来的好处。我们自身就是主线程,而电饭锅就是一个任务队列的任务,它会自己处理自己的事情,我们并不需要关心,只需要等待结果即可。就像我们请求后台任务,等待返回结果即可,此时我们的主线程不需要等待结果,还可以做其他事情,有了结果再调用一下就可以了。
上张图说明关系:
上例转换代码:
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)中,永远基于上面提到的执行顺序:主线程 > 微任务 > 宏任务。
举个简单的例子
看如下代码深入理解:
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");
});
这个例子还是很有特点的,要搞明白了它,就对事件循环没什么难理解的了。