20 分钟带你掌握 JavaScript Promise 和 Async/Await

2020-12-11 09:41:02 +08:00
 GrapeCityChina

一般在开发中,查询网络 API 操作时往往是比较耗时的,这意味着可能需要一段时间的等待才能获得响应。因此,为了避免程序在请求时无响应的情况,异步编程就成为了开发人员的一项基本技能。

在 JavaScript 中处理异步操作时,通常我们经常会听到 "Promise "这个概念。但要理解它的工作原理及使用方法可能会比较抽象和难以理解。

那么,在本文中我们将会通过实践的方式让你能更快速的理解它们的概念和用法,所以与许多传统干巴巴的教程都不同,我们将通过以下四个示例开始:

示例 1:用生日解释 Promise 基础知识

首先,我们先来看看 Promise 的基本形态是什么样的。

Promise 执行时分三个状态:pending (执行中)、fulfilled (成功)、rejected (失败)。

new Promise(function(resolve, reject) {
    if (/* 异步操作成功 */) {
        resolve(value); //将 Promise 的状态由 padding 改为 fulfilled
    } else {
        reject(error); //将 Promise 的状态由 padding 改为 rejected
    }
})

实现时有三个原型方法 then 、catch 、finally

promise
.then((result) => {
	//promise 被接收或拒绝继续执行的情况
})
.catch((error) => {
	//promise 被拒绝的情况
})
.finally (() => {
	//promise 完成时,无论如何都会执行的情况
})

基本形态介绍完成了,那么我们下面开始看看下面的示例吧。

用户故事:我的朋友 Kayo 答应在两周后在我的生日 Party 上为我做一个蛋糕。

如果一切顺利且 Kayo 没有生病的话,我们就会获得一定数量的蛋糕,但如果 Kayo 生病了,我们就没有蛋糕了。但不论有没有蛋糕,我们仍然会开一个生日 Party 。

所以对于这个示例,我们将如上的背景故事翻译成 JS 代码,首先让我们先创建一个返回 Promise 的函数。

const onMyBirthday = (isKayoSick) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!isKayoSick) {
        resolve(2);
      } else {
        reject(new Error("I am sad"));
      }
    }, 2000);
  });
};

在 JavaScript 中,我们可以使用 new Promise()创建一个新的 Promise,它接受一个参数为:(resolve,reject)=>{} 的函数。

在此函数中,resolve 和 reject 是默认提供的回调函数。让我们仔细看看上面的代码。

当我们运行 onMyBirthday 函数 2000ms 后。

现在,因为 onMyBirthday()返回的是一个 Promise,我们可以访问 then 、catch 和 finally 方法。我们还可以访问早些时候在 then 和 catch 中使用传递给 resolve 和 reject 的参数。

让我们通过如下代码来理解概念

如果 Kayo 没有生病

onMyBirthday(false)
  .then((result) => {
    console.log(\`I have ${result} cakes\`); // 控制台打印“I have 2 cakes”  
  })
  .catch((error) => {
    console.log(error); // 不执行
  })
  .finally(() => {
    console.log("Party"); // 控制台打印“Party”
  });

如果 Kayo 生病

onMyBirthday(true)
  .then((result) => {
    console.log(\`I have ${result} cakes\`); // 不执行 
  })
  .catch((error) => {
    console.log(error); // 控制台打印“我很难过”
  })
  .finally(() => {
    console.log("Party"); // 控制台打印“Party”
  });

相信通过这个例子你能了解 Promise 的基本概念。

下面我们开始示例 2

示例 2:一个猜数字的游戏

基本需求:

对于上面的需求,我们首先创建一个 enterNumber 函数并返回一个 Promise:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    // 从这开始编码
  });
};

我们要做的第一件事是向用户索要一个数字,并在 1 到 6 之间随机选择一个数字:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
    const randomNumber = Math.floor(Math.random() * 6 + 1); // 选择一个从 1 到 6 的随机数
  });
};

当用户输入一个不是数字的值。这种情况下,我们调用 reject 函数,并抛出错误:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
    const randomNumber = Math.floor(Math.random() * 6 + 1); //选择一个从 1 到 6 的随机数

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // 当用户输入的值非数字,抛出异常并调用 reject 函数
    }
  });
};

下面,我们需要检查 userNumber 是否等于 RanomNumber,如果相等,我们给用户 2 分,然后我们可以执行 resolve 函数来传递一个 object { points: 2, randomNumber } 对象。

如果 userNumber 与 randomNumber 相差 1,那么我们给用户 1 分。否则,我们给用户 0 分。

return new Promise((resolve, reject) => {
  const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
  const randomNumber = Math.floor(Math.random() * 6 + 1); // 选择一个从 1 到 6 的随机数

  if (isNaN(userNumber)) {
    reject(new Error("Wrong Input Type")); // 当用户输入的值非数字,抛出异常并调用 reject 函数
  }

  if (userNumber === randomNumber) {
    // 如果相等,我们给用户 2 分
    resolve({
      points: 2,
      randomNumber,
    });
  } else if (
    userNumber === randomNumber - 1 ||
    userNumber === randomNumber + 1
  ) {
    // 如果 userNumber 与 randomNumber 相差 1,那么我们给用户 1 分
    resolve({
      points: 1,
      randomNumber,
    });
  } else {
    // 否则用户得 0 分
    resolve({
      points: 0,
      randomNumber,
    });
  }
});

下面,让我们再创建一个函数来询问用户是否想继续游戏:

const continueGame = () => {
  return new Promise((resolve) => {
    if (window.confirm("Do you want to continue?")) { // 向用户询问是否要继续游戏
      resolve(true);
    } else {
      resolve(false);
    }
  });
};

为了不使游戏强制结束,我们创建的 Promise 没有使用 Reject 回调。

下面,我们创建一个函数来处理猜数字逻辑:

const handleGuess = () => {
  enterNumber() // 返回一个 Promise 对象
    .then((result) => {
      alert(\`Dice: ${result.randomNumber}: you got ${result.points} points\`); // 当 resolve 运行时,我们得到用户得分和随机数 
      
      // 向用户询问是否要继续游戏
      continueGame().then((result) => {
        if (result) {
          handleGuess(); // If yes, 游戏继续
        } else {
          alert("Game ends"); // If no, 弹出游戏结束框
        }
      });
    })
    .catch((error) => alert(error));
};

handleGuess(); // 执行 handleGuess 函数

在这当我们调用 handleGuess 函数时,enterNumber()返回一个 Promise 对象。

如果 Promise 状态为 resolved,我们就调用 then 方法,向用户告知竞猜结果与得分,并向用户询问是否要继续游戏。

如果 Promise 状态为 rejected,我们将显示一条用户输入错误的信息。

不过,这样的代码虽然能解决问题,但读起来还是有点困难。让我们后面将使用 async/await 对 hanldeGuess 进行重构。

网上对于 async/await 的解释已经很多了,在这我想用一个简单概括的说法来解释:async/await 就是可以把复杂难懂的异步代码变成类同步语法的语法糖

下面开始看重构后代码吧:

const handleGuess = async () => {
  try {
    const result = await enterNumber(); // 代替 then 方法,我们只需将 await 放在 promise 前,就可以直接获得结果

    alert(\`Dice: ${result.randomNumber}: you got ${result.points} points\`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // catch 方法可以由 try, catch 函数来替代
    alert(error);
  }
};

通过在函数前使用 async 关键字,我们创建了一个异步函数,在函数内的使用方法较之前有如下不同:

下面是我们重构后的完整代码,供参考:

const enterNumber = () => {
  return new Promise((resolve, reject) => {
    const userNumber = Number(window.prompt("Enter a number (1 - 6):")); // 向用户索要一个数字
    const randomNumber = Math.floor(Math.random() * 6 + 1); // 系统随机选取一个 1-6 的数字

    if (isNaN(userNumber)) {
      reject(new Error("Wrong Input Type")); // 如果用户输入非数字抛出错误
    }

    if (userNumber === randomNumber) { // 如果用户猜数字正确,给用户 2 分
      resolve({
        points: 2,
        randomNumber,
      });
    } else if (
      userNumber === randomNumber - 1 ||
      userNumber === randomNumber + 1
    ) { // 如果 userNumber 与 randomNumber 相差 1,那么我们给用户 1 分
      resolve({
        points: 1,
        randomNumber,
      });
    } else { // 不正确,得 0 分
      resolve({
        points: 0,
        randomNumber,
      });
    }
  });
};

const continueGame = () => {
  return new Promise((resolve) => {
    if (window.confirm("Do you want to continue?")) { // 向用户询问是否要继续游戏
      resolve(true);
    } else {
      resolve(false);
    }
  });
};

const handleGuess = async () => {
  try {
    const result = await enterNumber(); // await 替代了 then 函数

    alert(\`Dice: ${result.randomNumber}: you got ${result.points} points\`);

    const isContinuing = await continueGame();

    if (isContinuing) {
      handleGuess();
    } else {
      alert("Game ends");
    }
  } catch (error) { // catch 方法可以由 try, catch 函数来替代
    alert(error);
  }
};

handleGuess(); // 执行 handleGuess 函数

我们已经完成了第二个示例,接下来让我们开始看看第三个示例。

示例 3:从 Web API 中获取国家信息

一般当从 API 中获取数据时,开发人员会精彩使用 Promises 。如果在新窗口打开 https://restcountries.eu/rest/v2/alpha/cn,你会看到 JSON 格式的国家数据。

通过使用Fetch API,我们可以很轻松的获得数据,以下是代码:

const fetchData = async () => {
  const res = await fetch("https://restcountries.eu/rest/v2/alpha/cn"); // fetch() returns a promise, so we need to wait for it

  const country = await res.json(); // res is now only an HTTP response, so we need to call res.json()

  console.log(country); // China's data will be logged to the dev console
};

fetchData();

现在我们获得了所需的国家 /地区数据,让我们转到最后一项任务。

示例 4:从 Web API 中获取一个国家的周边国家列表

下面的 fetchCountry 函数从示例 3 中的 api 获得国家信息,其中的参数 alpha3Code 是代指该国家的国家代码,以下是代码

// Task 4: 获得中国周边的邻国信息
const fetchCountry = async (alpha3Code) => {
  try {
    const res = await fetch(
      \`https://restcountries.eu/rest/v2/alpha/${alpha3Code}\`
    );

    const data = await res.json();

    return data;
  } catch (error) {
    console.log(error);
  }
};

下面让我们创建一个 fetchCountryAndNeighbors 函数,通过传递 cn 作为 alpha3code 来获取中国的信息。

const fetchCountryAndNeighbors = async () => {
  const china= await fetchCountry("cn");

  console.log(china);
};

fetchCountryAndNeighbors();

在控制台中,我们看看对象内容:

在对象中,有一个 border 属性,它是中国周边邻国的 alpha3codes 列表。

现在,如果我们尝试通过以下方式获取邻国信息。

const neighbors = 
    china.borders.map((border) => fetchCountry(border));

neighbors 是一个 Promise 对象的数组。

当处理一个数组的 Promise 时,我们需要使用 Promise.all 。

const fetchCountryAndNeigbors = async () => {
  const china = await fetchCountry("cn");

  const neighbors = await Promise.all(
    china.borders.map((border) => fetchCountry(border))
  );

  console.log(neighbors);
};

fetchCountryAndNeigbors();

在控制台中,我们应该能够看到国家 /地区对象列表。

以下是示例 4 的所有代码,供您参考:

const fetchCountry = async (alpha3Code) => {
  try {
    const res = await fetch(
      \`https://restcountries.eu/rest/v2/alpha/${alpha3Code}\`
    );
    const data = await res.json();
    return data;
  } catch (error) {
    console.log(error);
  }
};

const fetchCountryAndNeigbors = async () => {
  const china = await fetchCountry("cn");
  const neighbors = await Promise.all(
    china.borders.map((border) => fetchCountry(border))
  );
  console.log(neighbors);
};

fetchCountryAndNeigbors();

总结

完成这 4 个示例后,你可以看到 Promise 在处理异步操作或不是同时发生的事情时很有用。相信在不断的实践中,对它的理解会越深、越强,希望这篇文章能对大家理解 Promise 和 Async/Await 带来一些帮助。

以下是本文中使用的代码:

Promise-Async-Await-main.zip

1875 次点击
所在节点    推广
5 条回复
GrapeCityChina
2020-12-11 10:11:01 +08:00
扩展阅读:推荐一款功能布局与 Excel 高度类似的纯前端表格控件 SpreadJS ( https://www.grapecity.com.cn/developer/spreadjs
Incrus
2020-12-11 10:18:34 +08:00
``` const data = await res.json(); ```
为啥转 json 也要 加 await ?
Hanser002
2020-12-11 10:34:54 +08:00
@Incrus
Body mixin 的 json() 方法接收一个 Response 流,并将其读取完成。它返回一个 Promise,Promise 的解析 resolve 结果是将文本体解析为 JSON 。
https://developer.mozilla.org/zh-CN/docs/Web/API/Body/json
Incrus
2020-12-11 13:37:26 +08:00
rodrick
2020-12-12 09:04:14 +08:00
多谢,看完以后已经能闭着眼手写 promise 了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/734351

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX