在这个模块,我们将查看异步 JavaScript,异步为什么很重要,以及怎样使用异步来有效处理潜在的阻塞操作,比如从服务器上获取资源。
浏览器提供的许多功能(尤其是最有趣的那一部分)可能需要很长的时间来完成,因此需要异步完成,例如:
我们先看下面的示例:
const ``name = ``"Miriam"``;```const greeting = Hello, my name` `is` `${``name}!;```console.log(greeting);``// ``"Hello, my name is Miriam!" |
在上面的代码中,声明了一个叫 name 的字符串常量,然后声明了一个 greeting 的字符串常量。最后将 greeting 输出到控制台中。
在浏览器中也是按照我们书写的代码顺序一行行执行,浏览器会等待代码的解析和执行,在上一行完成后执行下一行,即每一行的代码都是建立在前面的代码基础上。也使得它成为一个同步程序。
function makeGreeting(``name``) {`` ``return `Hello, my ```name` `is` `${name}!`;}const name = "Miriam";const greeting = makeGreeting(name);console.log(greeting);// "Hello, my name is Miriam!"` |
在这里 makeGreeting() 就是一个同步函数,因为在函数返回之前,调用者必须等待函数完成其工作。
对于一个耗时的函数,势必会造成页面的卡顿,那一般的处理策略就是引入回调,当函数开始执行时立即返回,等待执行结束后通过回调将结果进行返回。
类似于点击事件的监听,回调处理是一种特殊类型的回调函数,回调函数是一个被传递到另一个函数中会在适当时机被调用的函数。但是回调函数相互嵌套会让代码难以理解。
观察下面的同步函数:
function doStep1(init) {`` ``return init + 1;``}``function doStep2(init) {`` ``return init + 2;``}``function doStep3(init) {`` ``return init + 3;``}``function doOperation() {`` ``let result = 0;`` ``result = doStep1(result);`` ``result = doStep2(result);`` ``result = doStep3(result);`` ```console.log(结果:${result});```}``doOperation(); |
现在我们有一个被分成三步的操作,每一步都依赖于上一步。在这个例子中,第一步给输入的数据加 1,第二步加 2,第三步加 3。从输入 0 开始,最终结果是 6(0+1+2+3)。作为同步代码,这很容易理解。但是如果我们用回调来实现这些步骤呢?
function doStep1(init, callback) {`` ``const result = init + 1;`` ``callback(result);``}``function doStep2(init, callback) {`` ``const result = init + 2;`` ``callback(result);``}``function doStep3(init, callback) {`` ``const result = init + 3;`` ``callback(result);``}``function doOperation() {`` ``doStep1(0, (result1) => {`` ``doStep2(result1, (result2) => {`` ``doStep3(result2, (result3) => {`` ```console.log(结果:${result3});``` ``});`` ``});`` ``});``}``doOperation(); |
因为必须在回调函数中调用回调函数,我们就得到了这个深度嵌套的 doOperation() 函数,这就更难阅读和调试了。在一些地方这被称为 “回调地狱” 或 “厄运金字塔”(因为缩进看起来像一个金字塔的侧面)。
面对这样的嵌套回调,处理错误也会变得非常困难:你必须在 “金字塔” 的每一级处理错误,而不是在最高一级一次完成错误处理。
由于以上这些原因,大多数现代异步 API 都不使用回调。事实上,JavaScript 中异步编程的基础是 Promise
Promise 是现代 JavaScript 中的异步编程基础。它是一个由异步函数返回的对象,可以指示操作当前所处的状态。在 Promise 给调用者的时候,操作往往没有完成,但 Promise 对象提供了方法来处理最终的成功或失败。
首先,Promise 有三种状态:
有时我们用已敲定(settled)这个词来同时表示已兑现(fulfilled)和已拒绝(rejected)两种情况。
如果一个 Promise 已敲定,或者如果它被 “锁定” 以跟随另一个 Promise 的状态,那么它就是已解决(resolved)的。
比如定义一个网络请求:
const fetchPromise = ``fetch``(`` ``" |
在这个实例中,执行过程如下:
Promise 的优雅之处在于 then () 本身也会返回一个 Promise,这个 Promise 将指示 then () 中调用的异步函数的完成状态。所以对于嵌套场景时就可以改成链式。
const fetchPromise = ``fetch``(`` ``"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"``,``); fetchPromise`` ``.``then``((response) => response.json())`` ``.``then``((data) => {`` ``console.log(data[0].``name``);`` ``}); |
不必在第一个 then() 的处理程序中调用第二个 then(),我们可以直接返回 json() 返回的 Promise,并在该返回值上调用第二个 then()。这被称为 Promise 链,意味着当我们需要连续进行异步函数调用时,我们就可以避免不断嵌套带来的缩进增加。
当多个异步操作时,出现多个 Promise 返回值,此时,需要我们对 Promise 进行合并。这里就需要使用 Promise.all () 方法。
const fetchPromise1 = ``fetch``(`` ``" |
由 Promise.all() 返回的 Promise:
async 关键字为你提供了一种更简单的方法来处理基于异步 Promise 的代码。在一个函数的开头添加 async,就可以使其成为一个异步函数。在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。
async ``function myFunction() {`` ``// 这是一个异步函数``} |
比如:
async ``function f() {`` ``return 1;``} f().``then``(alert); // 1 |
所以说,async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。很简单,对吧?但不仅仅这些。还有另外一个叫 await 的关键词,它只在 async 函数内工作,也非常酷。
语法如下:
// 只在 async 函数内工作``let value = await promise; |
关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
在异步函数中,你可以在调用一个返回 Promise 的函数之前使用 await 关键字。这使得代码在执行时进行等待,直到 Promise 被完成,这时 Promise 的影响被当做返回值,或者被拒绝的响应作为错误抛出。
async ``function fetchProducts() {`` ``try {`` ``` // 在这一行之后,我们的函数将等待 fetch()调用完成``` ``` // 调用 fetch() 将返回一个“响应”或抛出一个错误``` ``const response = await ``fetch``(`` ``" |
这里我们调用 await fetch(),我们的调用者得到的并不是 Promise,而是一个完整的 Response 对象,就好像 fetch() 是一个同步函数一样。请注意你只能在 async 函数中使用 await。
Promise() 构造器使用单个函数作为参数。我们把这个函数称作执行器(executor)。当你创建一个新的 promise 的时候你需要实现这个执行器。
这个执行器本身采用两个参数,这两个参数都是函数,通常被称作 resolve 和 reject。在你的执行器实现里,你调用原始的异步函数。如果异步函数成功了,就调用 resolve,如果失败了,就调用 reject。如果执行器函数抛出了一个错误,reject 会被自动调用。你可以将任何类型的单个参数传递到 resolve 和 reject 中。
function alarm(person, delay) {`` ``return new Promise((resolve, reject) => {`` ``if (delay < 0) {`` ``throw new Error(``"Alarm delay must not be negative"``);`` ``}`` ``window.setTimeout(() => {`` ```resolve(Wake up, ${person}!);``` ``}, delay);`` ``});``} |
此函数创建并且返回一个新的 Promise。对于执行器中的 promise,我们:
Promise 是现代 JavaScript 异步编程的基础。它避免了深度嵌套回调,使表达和理解异步操作序列变得更加容易,并且它们还支持一种类似于同步编程中 try...catch 语句的错误处理方式。
async 和 await 关键字使得从一系列连续的异步函数调用中建立一个操作变得更加容易,避免了创建显式 Promise 链,并允许你像编写同步代码那样编写异步代码。