JavaScript是一种同步(synchronous
)且单线程的语言,那为什么又变成异步(asynchronous
)了?
首先,异步JavaScript是一种允许代码并发运行而不阻塞其他代码执行的编程模式。相较于同步代码,它独立于其它代码去执行,从而可以提高Web应用程序的性能和响应速度。
其次,从用处上来讲,异步代码也特别有用,比如,我们代码经常需要与外部资源(例如,服务器和数据库)交互,这可能会导致延迟并减慢代码的执行速度。通过使用异步技术,开发人员可以避免这些延迟,并允许其他代码在等待资源可用时继续执行。
接下来我们从如何实现 和如何执行两个方面来掌握异步JS。
一、异步JavaScript如何实现?
01. 使用回调
实现异步JavaScript的一种常见技术就是使用回调。回调是作为参数传递给另一个函数的函数,并在该函数完成后执行。
举个例子,代码如下:
js
setTimeout(function() {
console.log('executed after a 3 second delay.');
}, 3000)
这段代码,3秒后会在控制台打印结果,也就是说3秒延迟后会执行回调函数。同时,这3秒期间允许其他代码继续执行。
02. 使用Promise
实现异步JavaScript另一种技术是使用Promise
。Promise
是一个对象,它表示异步操作的最终成功或失败,并提供处理该操作结果的机制。
举个例子,代码如下:
js
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 3000);
});
promise.then((result) => {
console.log(result);
// 输出:Success
});
这段代码,我们使用构建函数创建了一个Promise
,同时使用定时器,设置了3秒后成功返回Promise
值,Promise
通过then
方法接收该返回值,同时,在此期间,允许其他代码继续执行。
03. 使用async/await
异步JavaScript还可以通过使用async/await
语法来实现,这种方式提供了一种更简洁,更可读的异步代码编写方式。
举个例子,代码如下:
js
async function example() {
const result = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 3000);
});
console.log(result);
// 输出:Success
}
example();
在这段代码中,我们使用async
定义了一个函数,并使用构造函数创建了一个Promise
,同样设置3秒后成功返回Promise
值,然后,我们使用关键字await
等待Promise
返回 ,并将结果存储在变量result
中。执行期间,允许函数之外的其他代码继续执行。
二、异步JavaScript如何执行?
编写JavaScript异步代码时,了解JavaScript运行时如何处理和执行任务至关重要。这就需要你充分理解调用堆栈、事件循环、Web API、回调队列和微任务队列等概念。 你也许已经看出来了,这些概念其实说的就是要掌握和理解事件循环的执行机制。
关于JavaScript事件循环,我画了一张图,如下:
接下来对着这张图,我逐一讲一下这些概念。
01. 调用堆栈
调用堆栈是JavaScript用于管理函数调用的数据结构。它以 后进先出(LIFO) 为基础工作,这意味着最近添加的函数将首先执行。当一个函数被调用时,它被添加到堆栈的顶部。当函数返回时,它被从堆栈中删除。
关于堆栈的出入栈规则,我画了张图来理解。
图如下所示:
[入栈演示图]
[出栈演示图]
接下来我举个例子,帮大家详细理解一下具体代码的执行过程。
代码如下:
js
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
foo();
}
bar();
这段代码中,当bar
函数被调用时,它被添加到调用堆栈的顶部。然后该bar
函数调用该foo
函数,该函数将添加到调用堆栈的顶部。当foo
函数返回时,它会从堆栈中删除,然后是函数bar
。
02. 事件循环
事件循环是JavaScript用于管理异步任务的机制。它会不断的检查任务队列,以查看是否有任何任务等待执行。如果有,它将任务添加到调用堆栈中。
同样,我们举个例子来讲一下
代码如下:
js
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
console.log('end');
在这个例子中,console.log('start')
和console.log('end')
语句被添加到调用堆栈并首先执行。setTimeout
然后调用该函数,这是一个异步任务。该setTimeout
功能已添加到Web API等待下一步往任务队列中推待执行的回调内容。
03. Web API
Web API是浏览器提供的一组API
,允许JavaScript
与浏览器环境进行交互。这些API
包括setTimeout
、setInterval
和fetch
函数。
当调用 Web API中的函数时,它会被添加到 Web APIs中。Web APIs管理任务并在完成时将其添加到任务队列。
04. 回调队列
回调队列是一种存储异步任务回调的数据结构。当异步任务完成时,其回调被添加到回调队列中。
队列(正向)的出入队规则如下:
上图很容易看懂,符合先进先出原则。下面我们举一个实例来看一下。
代码如下:
js
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
在这个实例中,setTimeout
函数和Promise
都是异步任务。当setTimeout
函数完成时,其回调被添加到回调队列 中。当Promise函数完成时,其回调将添加到微任务队列中。
05. 微任务队列
微任务队列与回调队列类似,但它用于微任务。微任务是当前任务完成后立即执行的函数。
同样,我举个例子来讲一下。
代码如下:
js
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('end');
在这个例子中,console.log('end')
语句被添加到调用堆栈 并首先执行。Promise
然后调用该函数,这是一个微任务。该Promise
函数的回调被添加到微任务队列中,并在当前任务完成后立即执行。
三、实例分解执行过程
任务分:同步任务(宏微任务)和异步任务(宏微任务)
什么是事件循环?
执行完宏任务,执行宏任务的微任务
执行另一个宏任务,执行另一个宏任务的微任务
01. 先来看一张流程图
从上图看出,事件循环就是不断执行宏任务及宏任务中的微任务的过程。
02. 再来看一个例子
js
new Promise(function(resolve) {
console.log('promise');
resolve();
}).then(function() {
console.log('then');
})
console.log('console');
这段代码作为宏任务,进入主线程。执行过程如下
第一步, 先遇到setTimeout,那么将其回调函数注册后分发到另一个宏任务。
第二步, 接下来遇到了Promise,new Promise
立即执行,输出promise
,then
函数分发到当前宏任务的微任务。
第三步, 遇到console.log()
,立即执行,输出console
。
第四步, 执行当前宏任务的微任务,输出then
。
第五步, 执行另一个宏任务,输出setTimeout
。
总结
了解调用堆栈、事件循环、Web API、回调队列和微任务队列对于编写高效且响应迅速的 JavaScript 代码至关重要。