JavaScript/异步
在没有特定关键字和技术的情况下,JavaScript 引擎会按照书写代码的顺序,依次执行语句。在大多数情况下,这是必要的,因为一行代码的结果会用在下一行代码中。
"use strict";
/* Line 1 */ const firstName = "Mahatma";
/* Line 2 */ const familyName = "Gandhi";
/* Line 3 */ const completeName = firstName + " " + familyName;
第 1 行和第 2 行必须完全完成,才能执行第 3 行。这是通常的顺序行为。
但有些情况,后面的语句不需要等待当前语句执行结束。或者,你预期某个活动会运行很长时间,你想要在这期间做些别的事情。这种并行执行有可能显著地减少整体响应时间。这是可能的,因为现代计算机拥有多个 CPU,能够同时执行多个任务。在上面的例子中,第 1 行和第 2 行可以并行运行。此外,客户端/服务器架构在多个服务器之间分配任务。
典型的情况是长时间运行的数据库更新、处理大型文件或 CPU 密集型计算。但即使对于渲染 HTML 页面这样看起来很简单的事情,浏览器通常也运行多个任务。
原生,“JavaScript 是单线程的,所有 JavaScript 代码都在单个线程中执行。这包括你的程序源代码和你包含在你程序中的第三方库。当程序进行 I/O 操作来读取文件或网络请求时,这会阻塞主线程”[1]。
为了无论如何都实现同时活动的目标,浏览器、服务工作者、库和服务器提供了额外的适当接口。它们的初衷是解除 HTTP 和数据库请求的阻塞执行;一小部分集中在 CPU 密集型计算上。
在 JavaScript 语言级别,有三种技术可以实现异步 - 这感觉起来像同时性。
- 回调函数
- Promise
- 关键字
async
和await
基本技术是使用回调函数。这个术语用于作为参数传递给另一个函数的函数,特别是 - 但不仅仅是 - 用于实现异步行为的函数。
一个Promise 代表异步操作的最终完成或失败,包括其结果。它在这些异步运行操作结束后,引导进一步处理。
因为使用 .then
和 .catch
对Promise 进行评估可能会导致代码难以阅读 - 特别是如果它们嵌套 -,JavaScript 提供了关键字 async
和 await
。它们的用法生成与传统 JavaScript 中的 try .. catch .. finally
类似的井井有条的代码。但它们并没有实现额外的功能。相反,在底层,它们是基于Promise。
为了证明代码并不总是按照严格的顺序执行,我们使用一个脚本,其中包含一个 CPU 密集型计算,它在一个或多或少很大的循环中运行。根据你的计算机,它会运行几秒钟。
"use strict";
// function with CPU-intensive computation
async function func_async(upper) {
await null; // (1)
return new Promise(function(resolve, reject) { // (2)
console.log("Starting loop with upper limit: " + upper);
if (upper < 0) {
// an arbitrary test to generate a failure
reject(upper + " is negative. Abort.");
}
for (let i = 0; i < upper; i++) {
// an arbitrary math function for test purpose
const s = Math.sin(i); // (3)
}
console.log("Finished loop for: " + upper);
resolve("Computed: " + upper);
})
}
const doTask = function(arr) {
for (let i = 0; i < arr.length; i++) {
console.log("Function invocation with number: " + arr[i]);
func_async(array1[i]) // (4)
.then((msg) => console.log("Ok. " + msg))
.catch((msg) => console.log("Error. " + msg));
console.log("Behind invocation for number: " + arr[i]);
}
}
const array1 = [3234567890, 10, -30];
doTask(array1);
console.log("End of program. Really?");
预期输出
Function invocation with number: 3234567890
Behind invocation for number: 3234567890
Function invocation with number: 10
Behind invocation for number: 10
Function invocation with number: -30
Behind invocation for number: -30
End of program. Really?
Starting loop with upper limit: 3234567890
Finished loop for: 3234567890
Starting loop with upper limit: 10
Finished loop for: 10
Starting loop with upper limit: -30
Finished loop for: -30
Ok. Computed: 3234567890
Ok. Computed: 10
Error. -30 is negative. Abort.
- 异步函数
func_async
的核心是一个循环,其中进行了一次数学计算 [(3) 第 14 行]。循环需要多少时间取决于给定的参数。 func_async
的返回值不是一个简单值,而是一个Promise。[(2) 第 6 行]func_async
针对给定数组中的每个元素,被doTask
调用一次。[(4) 第 24 行]- 由于
asyn_func
的异步性质,函数doTask
在func_async
运行之前就完全执行了!这可以通过程序输出观察到。 await null
[(1) 第 5 行] 是一个虚拟调用。它会挂起func_async
的执行,让doTask
继续执行。如果你删除这个语句,输出结果将不同。结论:要让函数真正异步,你需要两个关键字,函数签名中的async
和函数体中的await
。- 如果你有一个工具可以详细观察你的计算机,你会发现
func_async
的三个调用不是在不同的 CPU 上同时运行,而是在一个接一个地运行(在同一或不同的 CPU 上)。
将函数作为参数传递给(异步)函数是 JavaScript 中的最初技术。我们将使用预定义的 setTimeout
函数来演示它的目的和优势。它接受两个参数。第一个是我们所说的回调函数。第二个是指定回调函数被调用后经过多少毫秒的一个数字。
"use strict";
function showMessageLater() {
setTimeout(showMessage, 3000); // in ms
}
function showMessage() {
alert("Good morning.");
}
showMessage(); // immediate invocation
showMessageLater(); // invocation of 'showMessage' after 3 seconds
如果 showMessage
被调用,它会立即运行。如果 showMessageLater
被调用,它会将 showMessage
作为回调函数传递给 setTimeout
,setTimeout
会在延迟 3 秒后执行它。
一个Promise 会跟踪(异步)函数是否已成功执行或已因错误终止,并确定接下来会发生什么,调用 .then
或 .catch
。
Promise 处于三种状态之一
- 挂起:初始状态,在“已解决”或“已拒绝”之前
- 已解决:函数成功完成之后:调用了
resolve
- 已拒绝:函数因错误而完成之后:调用了
reject
"use strict";
// here, we have only the definition!
function demoPromise() {
// The 'return' statement is executed immediately, and the calling program
// continues its execution. But the value of 'return' keeps undefined until
// either 'resolve' or 'reject' is executed here.
return new Promise(function(resolve, reject) {
//
// perform some time-consuming actions like an
// access to a database
//
const result = true; // for example
if (result === true) {
resolve("Demo worked.");
} else {
reject("Demo failed.");
}
})
}
demoPromise() // invoke 'demoPromise'
.then((msg) => console.log("Ok: " + msg))
.catch((msg) => console.log("Error: " + msg));
console.log("End of script reached. But the program is still running.");
预期输出
End of script reached. But the program is still running.
Ok: Demo worked.
当 demoPromise
被调用时,它会创建一个新的Promise 并返回它。然后,它会执行耗时的操作,并根据结果调用 resolve
或 reject
。
接下来(在此期间,调用脚本已执行了其他操作),demoPromise
调用后的 .then()
或 .catch()
函数中的一个会被执行。这两个函数都接受一个(匿名)函数作为参数。传递给匿名函数的参数是Promise 的值。
.then( ( msg ) => console.log("Ok: " + msg) )
| | | | | | | | | |
| | | └── parameter ──┘ └────── funct. body ────┘ | | |
| | | | | |
| | └───────────── anonymous function ───────────────┘ | |
| | | |
| └─────── parameter of then() function ───────────────┘ |
| |
└─────────────────── then() function ──────────────────────┘
请注意,脚本的最后一条语句已之前执行了。
许多库、API 和服务器函数的接口都是定义和实现为返回一个Promise 的函数,类似于上面的 demoPromise
。只要你不必创建自己的异步函数,你就没有必要创建Promise。通常,调用外部接口并只使用 .then
或 .catch
(或下一章的 async
或 await
)就足够了。
关键字 await
会强制 JavaScript 引擎在执行下一条语句之前,完全运行 await
后面的脚本 - 包括 new Promise
部分。因此 - 从调用脚本的角度来看 - 异步行为被去除了。带有 await
的函数必须在它们的签名中用关键字 async
标记。
"use strict";
// same as above
function demoPromise() {
return new Promise(function(resolve, reject) {
const result = true; // for example
if (result === true) {
resolve("Demo worked.");
} else {
reject("Demo failed.");
}
})
}
// a function with the call to 'demoPromise'
// the keyword 'async' is necessary to allow 'await' inside
async function start() {
try {
// use 'await' to wait for the end of 'demoPromise'
// before executing the following statement
const msg = await demoPromise();
// without 'await', 'msg' contains the Promise, but without
// the success- or error-message
console.log("Ok: " + msg);
} catch (msg) {
console.log("Error: " + msg);
}
}
start();
console.log("End of script reached. End of program?");
使用 async .. await
允许你使用传统的 try .. catch
语句,而不是 .then
和 .catch
。
我们使用免费提供的演示 API https://jsonplaceholder.typicode.com/
。它以 JSON 格式提供少量测试数据。
"use strict";
async function getUserData() {
// fetch() is an asynchronous function of the Web API. It returns a Promise
// see: https://mdn.org.cn/en-US/docs/Web/API/Fetch_API/Using_Fetch
await fetch('https://jsonplaceholder.typicode.com/users')
// for the use of 'response', see: https://mdn.org.cn/en-US/docs/Web/API/Response/json
// '.json()' reads the response stream
.then((response) => { return response.json() })
// in this case, 'users' is an array of objects (JSON format)
.then((users) => {
console.log(users); // total data: array with 10 elements
// loop over the ten array elements
for (const user of users) {
console.log(user.name + " / " + user.email);
}
})
.catch((err) => console.log('Some error occurred: ' + err.message));
}
// same with 'try / catch'
async function getUserData_tc() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
console.log(users);
console.log(users[0].name);
for (const user of users) {
console.log(user.name + " / " + user.email);
}
} catch (err) {
console.log('Some error occurred: ' + err.message);
}
}
getUserData();
getUserData_tc();
此例子的步骤为
await fetch()
:获取 URL 后面的数据。await
部分保证脚本在fetch
传递所有数据之前不会继续。json()
读取包含结果数据的流。- 结果数据是一个包含 10 个元素的数组。每个元素都以 JSON 格式,例如
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "[email protected]",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}
- 作为示例,该脚本在控制台中显示了完整数据及其部分内容。
注意:当你使用任意 URL 时,你可能会遇到一个CORS 错误。