简介:回调

JavaScript 主机(host)环境提供了许多函数,这些函数允许我们计划 异步 行为(action)。换句话说,我们现在开始执行的行为,但它们会在稍后完成。
例如,setTimeout 函数就是一个这样的函数。
脚本是“异步”调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行。
如果在 loadScript(…) 下面有任何其他代码,它们不会等到脚本加载完成才执行。

  1. loadScript('/my/script.js');
  2. // loadScript 下面的代码
  3. // 不会等到脚本加载完成才执行
  4. // ...

假设我们需要在新脚本加载后立即使用它。它声明了新函数,我们想运行它们。
但如果我们在 loadScript(…) 调用后立即执行此操作,这将不会有效。

  1. loadScript('/my/script.js'); // 这个脚本有 "function newFunction() {…}"
  2. newFunction(); // 没有这个函数!

自然情况下,浏览器可能没有时间加载脚本。
让我们添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:

  1. function loadScript(src, callback) {
  2. let script = document.createElement('script');
  3. script.src = src;
  4. script.onload = () => callback(script);
  5. document.head.append(script);
  6. }

现在,如果我们想调用该脚本中的新函数,我们应该将其写在回调函数中:

  1. loadScript('/my/script.js', function() {
  2. // 在脚本加载完成后,回调函数才会执行
  3. newFunction(); // 现在它工作了
  4. ...
  5. });

这是我们的想法:第二个参数是一个函数(通常是匿名函数),该函数会在行为(action)完成时运行。

Promise

Promise 是将“生产者代码”和“消费者代码”连接在一起的一个特殊的 JavaScript 对象。
Promise 对象的构造器(constructor)语法如下

  1. let promise = new Promise(function(resolve, reject) {
  2. // executor(生产者代码,“歌手”)
  3. });

传递给 new Promise 的函数被称为 executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。按照上面的类比:executor 就是“歌手”。
当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:

  • resolve(value) — 如果任务成功完成并带有结果 value。
  • reject(error) — 如果出现了 error,error 即为 error 对象。

所以总结一下就是:executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject。

消费者:then,catch,finally

Promise 对象充当的是 executor(“生产者代码”或“歌手”)和消费函数(“粉丝”)之间的连接,后者将接收结果或 error。可以通过使用 .then、.catch 和 .finally 方法为消费函数进行注册。

then

  1. promise.then(
  2. function(result) { /* handle a successful result */ },
  3. function(error) { /* handle an error */ }
  4. );

catch

finally

finally 是执行清理(cleanup)的很好的处理程序(handler),例如无论结果如何,都停止使用不再需要的加载指示符(indicator)。

  1. new Promise((resolve, reject) => {
  2. /* 做一些需要时间的事儿,然后调用 resolve/reject */
  3. })
  4. // 在 promise 为 settled 时运行,无论成功与否
  5. .finally(() => stop loading indicator)
  6. // 所以,加载指示器(loading indicator)始终会在我们处理结果/错误之前停止
  7. .then(result => show result, err => show error)

Promise 链

它看起来就像这样:

  1. new Promise(function(resolve, reject) {
  2. setTimeout(() => resolve(1), 1000); // (*)
  3. }).then(function(result) { // (**)
  4. alert(result); // 1
  5. return result * 2;
  6. }).then(function(result) { // (***)
  7. alert(result); // 2
  8. return result * 2;
  9. }).then(function(result) {
  10. alert(result); // 4
  11. return result * 2;
  12. });

它的理念是将 result 通过 .then 处理程序(handler)链进行传递。

返回 promise

.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。

更复杂的示例:fetch

在前端编程中,promise 通常被用于网络请求。那么,让我们一起来看一个相关的扩展示例吧。
我们将使用 fetch 方法从远程服务器加载用户信息。它有很多可选的参数,我们在 单独的一章 中对其进行了详细介绍,但是基本语法很简单:

  1. let promise = fetch(url);

执行这条语句,向 url 发出网络请求并返回一个 promise。当远程服务器返回 header(是在 全部响应加载完成前)时,该 promise 使用一个 response 对象来进行 resolve。
下面这段代码向 user.json 发送请求,并从服务器加载该文本:

  1. fetch('/article/promise-chaining/user.json')
  2. // 当远程服务器响应时,下面的 .then 开始执行
  3. .then(function(response) {
  4. // 当 user.json 加载完成时,response.text() 会返回一个新的 promise
  5. // 该 promise 以加载的 user.json 为 result 进行 resolve
  6. return response.text();
  7. })
  8. .then(function(text) {
  9. // ...这是远程文件的内容
  10. alert(text); // {"name": "iliakan", "isAdmin": true}
  11. });

为了简洁,我们还将使用箭头函数:

  1. // 同上,但是使用 response.json() 将远程内容解析为 JSON
  2. fetch('/article/promise-chaining/user.json')
  3. .then(response => response.json())
  4. .then(user => alert(user.name)); // iliakan, got user name

使用 promise 进行错误处理

Promise API

在 Promise 类中,有 5 种静态方法。我们在这里简单介绍下它们的使用场景。

Promise.all

假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。如果任意的 promise reject,则 Promise.all 整个将会 reject。

  1. let promise = Promise.all([...promises...]);

例如,下面的 Promise.all 在 3 秒之后被 settled,然后它的结果就是一个 [1, 2, 3] 数组:

  1. Promise.all([
  2. new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  3. new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  4. new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
  5. ]).then(alert); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素

Promise.allSettled

Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。

Promise.race

与 Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。

Promise.resolve/reject

在现代的代码中,很少需要使用 Promise.resolve 和 Promise.reject 方法,因为 async/await 语法(我们会在 稍后 讲到)使它们变得有些过时了。

Promisification

它指将一个接受回调的函数转换为一个返回 promise 的函数。
由于许多函数和库都是基于回调的,因此,在实际开发中经常会需要进行这种转换。

微任务(Microtask)

Promise 的处理程序(handlers).then、.catch 和 .finally 都是异步的。
即便一个 promise 立即被 resolve,.then、.catch 和 .finally 下面 的代码也会在这些处理程序(handler)之前被执行。

微任务队列(Microtask queue)

简单地说,当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序(handler)就会被放入队列中:但是它们不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。
Promise 的处理程序(handler)总是会经过这个内部队列。

未处理的 rejection

总结

Promise 处理始终是异步的,因为所有 promise 行为都会通过内部的 “promise jobs” 队列,也被称为“微任务队列”(ES8 术语)。
因此,.then/catch/finally 处理程序(handler)总是在当前代码完成后才会被调用。
如果我们需要确保一段代码在 .then/catch/finally 之后被执行,我们可以将它添加到链式调用的 .then中。

Async/await

Async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

Async function

让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:

  1. async function f() {
  2. return 1;
  3. }

在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。
例如,下面这个函数返回一个结果为 1 的 resolved promise,让我们测试一下:

  1. async function f() {
  2. return 1;
  3. }
  4. f().then(alert); // 1

……我们也可以显式地返回一个 promise,结果是一样的:

  1. async function f() {
  2. return Promise.resolve(1);
  3. }
  4. f().then(alert); // 1

所以说,async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。很简单,对吧?但不仅仅这些。还有另外一个叫 await 的关键词,它只在 async 函数内工作,也非常酷。

Await

  1. // 只在 async 函数内工作
  2. let value = await promise;

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
这里的例子就是一个 1 秒后 resolve 的 promise:

  1. async function f() {
  2. let promise = new Promise((resolve, reject) => {
  3. setTimeout(() => resolve("done!"), 1000)
  4. });
  5. let result = await promise; // 等待,直到 promise resolve (*)
  6. alert(result); // "done!"
  7. }
  8. f();

这个函数在执行的时候,“暂停”在了 () 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”。
让我们强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法,同时也更易于读写。
不能在普通函数中使用 await
*await 不能在顶层代码运行

  1. // 用在顶层代码中会报语法错误
  2. let response = await fetch('/article/promise-chaining/user.json');
  3. let user = await response.json();

但我们可以将其包裹在一个匿名 async 函数中,如下所示:

  1. (async () => {
  2. let response = await fetch('/article/promise-chaining/user.json');
  3. let user = await response.json();
  4. ...
  5. })();

async/await 和 promise.then/catch
当我们使用 async/await 时,几乎就不会用到 .then 了,因为 await 为我们处理了等待。并且我们使用常规的 try..catch 而不是 .catch。