Javascript 是单线程语言,代码执行从上到下排队执行。也就是一次只能执行一个任务,如果某个任务执行时间过长就会阻塞后面的任务执行,比如造成浏览器假死等。为了解决这个问题,就需要异步执行。
本文主要解决异步机制是怎么实现的。在解释异步机制前,需要先了解下同步机制。
执行如下一段代码:
// 处理数据
function processImage() {
console.log("图片处理完成")
}
// 请求数据
function networkRequest() {
console.log("数据请求成功")
greeting()
}
function greeting() {
console.log("Hello World!")
}
processImage()
networkRequest()
执行这段代码需要了解两个概念:执行上下文 和 执行栈。
执行上下文(Execution context)
在 JavaScript 中代码运行时需要创建执行上下文,全局代码有全局执行上下文,每个方法也有自己的执行上下文。
调用栈 (Call stack)
调用栈是运行 JavaScript 的地方,执行上下文被押入栈中才会被执行,执行完后被弹出,遵循先入后出,后入先出的原则。
了解了这两个概念后来看这段代码的执行过程:

最先押入栈的是 main() 全局执行上下文,每个方法在执行时都会押入栈中,当 main() 弹出,整段代码执行完毕。从中我们可以很容易发现任意方法执行时间很长的话,都会阻塞整个栈(或者线程)。
怎么解决?当然是引入异步执行
来看一段异步执行的代码:
const networkRequest = () => {
const callback = () => console.log('Async Code');
setTimeout(callback, 2000);
};
console.log('Hello World');
networkRequest();
// => 'Hello World'
// 等待 2s
// => 'Async Code'
我们用 setTimeout 模拟接口请求,需要注意的是 setTimeout 不属于 Javascript,它是由浏览器提供的 API(当然 Node.js 也有这个 API)。
执行到 setTimeout 时,交给浏览器提供的线程执行的,后续 console.log('Hello World') 继续执行,并不会等待 setTimeout 执行完后再执行,这就形成了异步执行。
当 setTimeout 执行完后,callback 被推入 消息队列(Message Queue) 中。此时 事件循环(The Event Loop) 会观察 Call Stack 是否清空,如果已清空,将 callback 押入栈中执行。如下动图:

总结
setTimeout,Promise,Async/Await 等都可实现异步,需要注意的是 ES6 为了引入 Promise 加入了微任务队列(Micro Task Queue),它的优先级高于消息队列(Message Queue),也就是说当调用栈清空后事件循环(The Event Loop)会率先将微任务队列的回调押入栈中执行,看如下代码:
function func() {
setTimeout(()=>{console.log('setTimeout')}, 0)
new Promise((resolve, reject)=>{
resolve('Promise')
}).then((msg) => {console.log(msg)})
}
func()
// => Promise
// => setTimeout
虽然 setTimeout 的代码先于 Promise 执行,但打印结果显示 Promise 的回调是先于 setTimeout 执行的。