JS 是单线程的,也就是同一个时刻只能做一件事情
宏任务和微任务
微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个。
常见宏任务:
- setTimeout()
- setInterval()
- setImmediate()
常见微任务:
- promise.then()、promise.catch()
- new MutaionObserver()
- process.nextTick()
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('同步代码2')
resolve()
}).then(() => {
console.log('promise.then')
})
console.log('同步代码3');
// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"
上面的代码将按如下顺序输出为:”同步代码 1″、”同步代码 2″、”同步代码 3″、”promise.then”、”setTimeout”,具体分析如下。
(1)setTimeout 回调和 promise.then 都是异步执行的,将在所有同步代码之后执行;
顺便提一下,在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms。具体值可能不固定,但不是为 0。
(2)虽然 promise.then 写在后面,但是执行顺序却比 setTimeout 优先,因为它是微任务;
(3)new Promise 是同步执行的,promise.then 里面的回调才是异步的。
下面我们看一下上面代码的执行过程演示:
也有人这样去理解:微任务是在当前事件循环的尾部去执行;宏任务是在下一次事件循环的开始去执行。
例题1
试着自己回答一下这道题,求打印顺序:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => {
console.log(6);
})
console.log(7);
解答:
第1步,console.log(1);,打印1
位置 | 任务 |
---|---|
调用栈 | console.log(1); |
微任务 | |
宏任务 |
第2步,setTimeout,不打印
位置 | 任务 |
---|---|
调用栈 | setTimeout |
微任务 | |
宏任务 | setTimeout的回调 |
第3步,new Promise的函数参数,注意,Promise的函数参数是同步执行的,打印4
位置 | 任务 |
---|---|
调用栈 | new Promise的函数参数 |
微任务 | new Promise的then的回调 |
宏任务 | setTimeout 的回调 |
第4步,第二个setTimeout,不打印
位置 | 任务 |
---|---|
调用栈 | 第二个setTimeout |
微任务 | new Promise的then的回调 |
宏任务 | setTimeout 的回调,第二个setTimeout的回调 |
第5步,console.log(7);,打印7
位置 | 任务 |
---|---|
调用栈 | console.log(7); |
微任务 | new Promise的then的回调 |
宏任务 | setTimeout 的回调,第二个setTimeout的回调 |
第6步,第一个微任务,即new Promise的then的回调,打印5
位置 | 任务 |
---|---|
调用栈 | new Promise的then的回调 |
微任务 | |
宏任务 | setTimeout的回调,第二个setTimeout的回调 |
第7步,第一个宏任务,即setTimeout的回调,打印2,同时,Promise.resolve().then()
的回调进入微队列
位置 | 任务 |
---|---|
调用栈 | setTimeout的回调 |
微任务 | Promise.resolve().then() 的回调 |
宏任务 | 第二个setTimeout的回调 |
第8步,第一个微任务,即Promise.resolve().then()
的回调,打印3
位置 | 任务 |
---|---|
调用栈 | Promise.resolve().then() 的回调 |
微任务 | |
宏任务 | 第二个setTimeout的回调 |
第9步,第一个宏任务,即第二个setTimeout的回调,打印6
位置 | 任务 |
---|---|
调用栈 | 第二个setTimeout的回调 |
微任务 | |
宏任务 |
是不是跟你演算的一样呢?
例题2
考察then方法链的执行顺序:
new Promise((resolve, reject) => {
console.log(1)
resolve(2)
}).then((data) => {
// 1号回调
console.log(data);
return 3
}).then((data) => {
// 2号回调
console.log(data);
})
new Promise((resolve, reject) => {
console.log(5)
resolve(6)
}).then((data) => {
// 3号回调
console.log(data);
return 7;
}).then((data) => {
// 4号回调
console.log(data);
})
你以为会打印1 2 3 5 6 7吗?错!你以为会打印1 5 2 3 6 7吗?也错!
- new Promise的函数参数是同步代码,所以先打印1。同时将第一个then的回调(1号回调)放入微队列。
- 同理,打印5,将第二个new Promise的第一个then的回调(3号回调)放入微队列。
- 执行1号回调,打印2,将2号回调放入微队列。
- 执行3号回调,打印6,将4号回调放入微队列。
- 执行2号回调,打印3。
- 执行4号回调,打印7。
所以结果是1 5 2 6 3 7,你演算对了吗?
例题3
有如下代码:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
})
})
不许修改这段代码,只允许在外层作用域添加代码,如何实现在打印1
跟打印2
中间插入打印3
?
其实这个题跟例题2类似,考察的也是基本知识:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
})
})
setTimeout(() => {
console.log(3)
setTimeout(() => {
console.log(4)
})
})
结果:打印 1 3 2 4。
原因:同例题2。
例题4
这次考察对async/await的执行顺序的理解:
console.log('script start');
async function async1() {
await async2();
console.log('async1 end');
};
async function async2() {
console.log('async2 end');
};
async1()
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve, reject) => {
console.log('promise start');
resolve()
})
.then(() => console.log('promise end'))
console.log('script end')
- 打印
script start
- 执行
async1()
,async1
的第一行是await async2()
,这一句应拆开考察,其中async2()
会同步执行,同时把await
下方的所有代码放到微队列。 - 执行setTimeout,不打印。同时把回调放入宏队列。
- 执行
new Promise
的函数参数,打印promise start
,同时把then的回调放入微队列 ,此时,微队列有2块代码,分别是await下面的代码(即console.log('async1 end');
),以及console.log('promise end')
。 - console.log(‘script end’),打印
script end
。 - 执行微队列第一个,也就是打印
async1 end
- 执行微队列第二个,也就是打印
promise end
- 执行宏任务,也就是打印
setTimeout
注意:低版本的Chrome浏览器(大约是70版本之前)会先打印promise end
,后打印async1 end
,原因我忘却了,大致是Chrome的早期实现里有Bug,其实原因也不重要,既然高版本的浏览器纠正归来了,就行了。
例题5
证明await的赋值操作是异步任务:
let x = 5;
let y;
function ret() {
x += 1;
console.log('x是', x);
console.log('y是', y);
return x;
}
async function a() {
y = await ret();
console.log(y);
}
async function b() {
y = await ret();
console.log(y);
}
a()
b()
得到:
x是 6
y是 undefined
x是 7
y是 undefined
6
7
我们用反证法,假定await赋值是同步操作,那么a()
的y = await ret()
会同步执行,之后才执行b()
的ret()
,此时y有值,不应该是undefined
,但事实是undefined
,说明await赋值是异步操作。
例题6
证明new Promise()的函数参数和await后面的函数是同步任务,证明很简单:
var i;
for (i = 0; i < 20; i++) {
new Promise(resolve => {
console.log(i)
})
}
也是反证法,假如new Promise()的函数参数是异步任务,那么应该像setTimeout
一样打印20个20
,然而事实上会打印等差数列。
注意,不要用for(let i = 0;)
这种写法,因为这会形成一个特殊作用域,不能反证出我们的结论。
function log(x) { console.log(x) }
async function a(i) {
await log(i)
}
var i;
for (i = 0; i < 20; i++) {
a(i);
}
function log(x) { console.log(x) }
var i;
for (i = 0; i < 20; i++) {
async function a() {
await log(i)
}
a();
}
同理,因为上面2段代码的结果也都是等差数列,所以await后面的语句是同步任务。
例题7
证明await会暂停for循环:
var i;
function ret() {
console.log('ret里的i', i);
return 100;
}
async function a() {
for (i = 0; i < 20; i++) {
await ret();
console.log(i);
}
}
a();
得到等差数列。
虽然微队列放入了20个任务,但是由于await会暂停循环,也就是说i并不会像setTimeout时一样自顾自的增长为20,而是会等待每一个微任务执行完毕,由此证明await会暂停for循环。
相反,setTimeout不会阻止for循环:
var i;
function ret() {
console.log('ret里的i', i);
return 100;
}
async function a() {
for (i = 0; i < 20; i++) {
setTimeout(() => {
ret();
console.log(i);
});
}
}
a();
总结
- 随时遇到异步,随时放入队列,这是起码的准则。
- 先同步任务,同步任务有异步回调的时候,根据规则放入队列。
- 同步任务都完成了就执行微队列。有异步则继续放入队列。
- 微队列都完成了就执行宏队列第一个。有异步则继续放入队列。
- 观察微队列,有则执行,没则执行宏队列第二个。
- 重复3/4/5步骤。