JavaScript的异步机制是怎么实现的?

3,470 阅读2分钟

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 押入栈中执行。如下动图:

总结

setTimeoutPromiseAsync/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 执行的。

Reference