跳转到内容

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
  • 关键字 asyncawait

基本技术是使用回调函数。这个术语用于作为参数传递给另一个函数的函数,特别是 - 但不仅仅是 - 用于实现异步行为的函数。

一个Promise 代表异步操作的最终完成或失败,包括其结果。它在这些异步运行操作结束后,引导进一步处理。

因为使用 .then.catchPromise 进行评估可能会导致代码难以阅读 - 特别是如果它们嵌套 -,JavaScript 提供了关键字 asyncawait。它们的用法生成与传统 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 的异步性质,函数 doTaskfunc_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 作为回调函数传递给 setTimeoutsetTimeout 会在延迟 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 并返回它。然后,它会执行耗时的操作,并根据结果调用 resolvereject

接下来(在此期间,调用脚本已执行了其他操作),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(或下一章的 asyncawait)就足够了。

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 错误

... 可在另一个页面上找到(点击这里)。

参考文献

[编辑 | 编辑源代码]
华夏公益教科书