同步和异步、微任务与宏任务
JavaScript 语言的一大特点就是单线程,也就是说同一个时间只能处理一个任务。上一个任务没有完成,下一个就要一直等着。如果排队是因为计算量大, CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
为了解决这个问题就用到了异步和同步执行任务的技术。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步任务里,对于异步类型还有进一步的划分,那就是接下来我们要讲的微任务和宏任务,切记微任务比宏任务先执行
Event Loop(事件循环)
- 初始状态下,调用栈为空。微任务队列为空,只有宏任务队列里有一个script 脚本(整体代码)。这时首先执行并出队的就是 整体代码。
- 整体代码进入调用栈,进行异步和同步的划分。
- 同步任务直接被执行并且推出栈。异步任务被划分为同步任务和异步任务,分别被推进微任务对列和宏任务队列
- 等同步任务执行完毕之后(调用栈为空)再执行微任务,将微任务压入调用栈
- 等微任务执行完毕之后(调用栈为空) 再执行宏任务,将宏任务压入调用栈,直至调用栈再一次为空,一次轮回结束。
宏任务
# | 浏览器 | Node |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
微任务
# | 浏览器 | Node |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
然后我们来看几道面试题
- 1.先来个简单的
console.log("1");
setTimeout(function () {
console.log("2");
}, 0);
Promise.resolve()
.then(function () {
console.log("3");
}).then(function () {
console.log("4");
});
console.log("5");
输出:1 5 3 4 2
1.调用栈空、微任务对列空。整个脚本作为宏任务,被执行,执行同步输出 1 5。
2.setTimeout宏任务放入宏任务队列。
3.Promise.then()微任务放入微任务执行队列。
2.同步任务执行完毕(调用栈为空),开始执行微任务,输出3 4
3.微任务执行完毕(调用栈为空),开始执行宏任务,输出 2
- 2.注意和上面的区别
console.log('1')
setTimeout(function () {
console.log("2");
}, 0);
new Promise((resolve, rejects) => {
console.log('3')
resolve()
}).then(function () {
console.log("4");
}).then(function () {
console.log("5");
});
console.log("6")
输出:1 3 6 4 5 2
答对了吗?
注意: Promise.then catch finally是微任务,但是romise构造函数中函数体的代码都是立即执行的。
new Promise(function (resolve) {
console.log(1)
})
上面这段实例代码的 1 ,是直接输出的,属于同步任务,虽然它确实在 Promise 中
- 3.复杂一点的
function app() {
setTimeout(() => {
console.log("1-1");
Promise.resolve().then(() => {
console.log("2-1");
});
});
console.log("1-2");
Promise.resolve().then(() => {
console.log("1-3");
setTimeout(() => {
console.log("3-1");
});
});
}
app();
输出:1-2 1-3 1-1 2-1 3-1
1.整个脚本作为宏任务被执行,首先执行同步任务输出:1-2
2.宏任务setTimeout,被放入宏任务队列等待执行
3.微任务 Promise.resolve().then,被放入微任务队列等待执行
4.同步任务执行完毕,调用栈此时为空,将微任务压入调用栈执行,输出1-3
5.又遇到一个宏任务,放入宏任务队列等待下一次轮询执行(和之前的宏任务不在同一个层级)
6.微任务执行完毕,调用栈此时为空,将宏任务压入调用栈执行,输出1-1
7.一次轮询结束
8.微任务执行,输出2-1
9.宏任务执行,输出3-1
关于async和await
promise的异步体现在 .then .catch .finally中,所以写在promise中的代码是同步执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。出现await之后呢?实际上await是一个让出线程的标志。await后面的表达式会立即执行,然后将后面代码放到微任务队列中,等待下一次轮询之后执行,然后就会跳出整个async函数来执行后面的代码。
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
相当于
async function async1() {
console.log('async1 start')
Promise.resolve(async2()).then(() => {
console.log('async1 end')
})
}
然后我们一起看个面试题练习一下吧
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0);
async1()
new Promise(resolve => {
console.log('promise1')
resolve()
})
.then(() => {
console.log('promise2')
})
console.log('script end')
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
new Promise(resolve => {
console.log('promise1')
resolve()
})
.then(() => {
console.log('promise2')
})
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0);
async1()
new Promise(resolve => {
console.log('promise3')
resolve()
})
.then(() => {
console.log('promise4')
})
console.log('script end')
async function async1() {
console.log('async1 start')
await async2()
setTimeout(() => {
console.log('setTimeout1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('setTimeout2')
}, 0)
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout3')
}, 0);
async1()
new Promise(resolve => {
console.log('promise1')
resolve()
})
.then(() => {
console.log('promise2')
})
console.log('script end')