• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-04-14 00:35 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

使用 promise 进行错误处理

概述

  1. promise 链在错误(error)处理中十分强大
  2. 当一个 promisereject 时,控制权将移交至最近的 rejection 处理程序
  3. 例如,下面代码中所 fetchURL 是错的(没有这个网站),.catch 对这个 error 进行了处理:
    1. 如你所看到的,.catch 不必是立即的。它可能在一个或多个 .then 之后出现
    2. 或者,可能该网站一切正常,但响应不是有效的 JSON

  1. 捕获所有 error 的最简单的方法是,将 .catch 附加到链的末尾:
    1. 通常情况下,这样的 .catch 根本不会被触发
    2. 但是如果上述任意一个 promise rejected(网络问题或者无效的 json 或其他),.catch 就会捕获它

隐式 try…catch

  1. promise 的执行者(executor)和 promise 的处理程序周围有一个“隐式的 try..catch
  2. 如果发生异常,它就会被捕获,并被视为 rejection 进行处理
  3. 例如,下面这段代码:

  1. 与下面这段代码工作上完全相同:

  1. executor 周围的“隐式 try..catch”自动捕获了 error,并将其变为 rejected promise
    1. 这不仅仅发生在 executor 函数中,同样也发生在其处理程序中
    2. 如果我们在 .then 处理程序中 throw,这意味着 promise rejected,因此控制权移交至最近的 error 处理程序
    3. 对于所有的 error 都会发生这种情况,而不仅仅是由 throw 语句导致的这些 error

  1. 例如,一个编程错误:
    1. 最后的 .catch 不仅会捕获显式的 rejection,还会捕获它上面的处理程序中意外出现的 error

再次抛出(Rethrowing

  1. 如我们已经注意到的,链尾端的 .catch 的表现有点像 try..catch
  2. 们可能有许多个 .then 处理程序,然后在尾端使用一个 .catch 处理上面的所有 error
    1. 在常规的 try..catch 中,我们可以分析 error,如果我们无法处理它,可以将其再次抛出
    2. 对于 promise 来说,这也是可以的
  3. 如果我们在 .catchthrow,那么控制权就会被移交到下一个最近的 error 处理程序
    1. 如果我们处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序
    2. 在下面这个例子中,.catch 成功处理了 error
    3. 这里 .catch 块正常完成。所以下一个成功的 .then 处理程序就会被调用

  1. 在下面的例子中,我们可以看到 .catch 的另一种情况
    1. (*) 行的处理程序捕获了 error,但无法处理它(例如,它只知道如何处理 URIError),所以它将其再次抛出:
    2. 执行从第一个 .catch (*) 沿着链跳转至下一个 (**)

未处理的 rejection

  1. 当一个 error 没有被处理会发生什么?例如,我们忘了在链的尾端附加 .catch,像这样:

  1. 如果出现 errorpromise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。但上面这个例子中并没有这样的处理程序
    1. 因此 error 会“卡住”。没有代码来处理它
  2. 当发生一个常规的 error 并且未被 try..catch 捕获时会发生什么?
    1. 脚本死了,并在控制台中留下了一个信息
    2. 对于在 promise 中未被处理的 rejection,也会发生类似的事
  3. JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error
    1. 在浏览器中,我们可以使用 unhandledrejection 事件来捕获这类 error

  1. 这个事件是 HTML 标准的一部分
    1. 如果出现了一个 error,并且在这没有 .catch,那么 unhandledrejection 处理程序就会被触发,并获取具有 error 相关信息的 event 对象,所以我们就能做一些后续处理了

Promise API

概述

  1. Promise 类中,有 6 种静态方法

Promise.all

  1. 假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪
    1. 例如,并行下载几个 URL,并等到所有内容都下载完毕后再对它们进行处理
  2. 语法:
    1. Promise.all 接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise
    2. 当所有给定的 promiseresolve 时,新的 promise 才会 resolve,并且其结果数组将成为新 promise 的结果

  1. 请注意,结果数组中元素的顺序与其在源 promise 中的顺序相同
    1. 即使第一个 promise 花费了最长的时间才 resolve,但它仍是结果数组中的第一个
    2. 一个常见的技巧是,将一个任务数据数组映射(map)到一个 promise 数组,然后将其包装到 Promise.all
    3. 例如,如果我们有一个存储 URL 的数组,我们可以像这样 fetch 它们:

  1. 一个更真实的示例,通过 GitHub 用户名来获取一个 GitHub 用户数组中用户的信息

  1. 如果任意一个 promisereject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error
    1. 这里的第二个 promise 在两秒后 reject。这立即导致了 Promise.allreject
    2. 因此 .catch 执行了:被 rejecterror 成为了整个 Promise.all 的结果

  1. Promise.all(iterable) 允许在 iterable 中使用非 promise 的“常规”值
    1. 通常,Promise.all(...) 接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数
    2. 但是,如果这些对象中的任何一个不是 promise,那么它将被“按原样”传递给结果数组
    3. 例如,这里的结果是 [1, 2, 3]
    4. 所以我们可以在方便的地方将准备好的值传递给 Promise.all

注意

  1. 如果出现 error,其他 promise 将被忽略
    1. 如果其中一个 promiserejectPromise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略

Promise.allSettled

  1. 这是一个最近添加到 JavaScript 的特性
  2. 如果任意的 promise reject,则 Promise.all 整个将会 reject
    1. 当我们需要 所有 结果都成功时,它对这种“全有或全无”的情况很有用:

  1. Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何
    1. 对成功的响应,结果数组对应元素的内容为 {status:"fulfilled", value:result}
    2. 对出现 error 的响应,结果数组对应元素的内容为 {status:"rejected", reason:error}
  2. 例如,我们想要获取(fetch)多个用户的信息。即使其中一个请求失败,我们仍然对其他的感兴趣

  1. 上面的 (*) 行中的 results 将会是:

  1. 如果浏览器不支持 Promise.allSettled,很容易进行 polyfill

Promise.race

  1. Promise.all 类似,但只等待第一个 settledpromise 并获取其结果(或 error
  2. 语法:

  1. 例如,这里的结果将是 1
    1. 这里第一个 promise 最快,所以它变成了结果
    2. 第一个 settledpromise “赢得了比赛”之后,所有进一步的 result/error 都会被忽略

Promise.any

  1. Promise.race 类似,区别在于 Promise.any 只等待第一个 fulfilledpromise,并将这个 fulfilledpromise 返回
  2. 如果给出的 promiserejected,那么返回的 promise 会带有 AggregateError—— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error
  3. 语法

  1. 例如,这里的结果将是 1
    1. 这里的第一个 promise 是最快的,但 rejected
    2. 所以第二个 promise 则成为了结果
    3. 在第一个 fulfilledpromise “赢得比赛”后,所有进一步的结果都将被忽略

  1. 这是一个所有 promise 都失败的例子:
    1. 我们在 AggregateError 错误类型的 error 实例的 errors 属性中可以访问到失败的 promiseerror 对象

Promise.resolve/reject

  1. 现代的代码中,很少需要使用 Promise.resolvePromise.reject 方法,因为 async/await 语法
  2. Promise.resolve(value) 用结果 value 创建一个 resolvedpromise
    1. 当一个函数被期望返回一个 promise 时,这个方法用于兼容性
    2. 这里的兼容性是指,我们直接从缓存中获取了当前操作的结果 value,但是期望返回的是一个 promise,所以可以使用 Promise.resolve(value)value “封装”进 promise,以满足期望返回一个 promise 的这个需求

  1. 例如,下面的 loadCached 函数获取(fetch)一个 URL 并记住其内容。以便将来对使用相同 URL 的调用
    1. 它能立即从缓存中获取先前的内容,但使用 Promise.resolve 创建了一个该内容的 promise,所以返回的值始终是一个 promise

  1. Promise.reject(error)error 创建一个 rejectedpromise

Promisification

概述

  1. 它指将一个接受回调的函数转换为一个返回 promise 的函数
  2. 由于许多函数和库都是基于回调的,因此,在实际开发中经常会需要进行这种转换
    1. 因为使用 promise 更加方便,所以将基于回调的函数和库 promise 化是有意义的
  3. 例如,在 回调 一章中我们有 loadScript(src, callback)
    1. 该函数通过给定的 src 加载脚本,然后在出现错误时调用 callback(err),或者在加载成功时调用 callback(null, script)

  1. 现在,让我们将其 promise 化吧
    1. 我们将创建一个新的函数 loadScriptPromise(src),与上面的函数作用相同(加载脚本),只是我们创建的这个函数会返回一个 promise 而不是使用回调
    2. 换句话说,我们仅向它传入 src(没有 callback)并通过该函数的 return 获得一个 promise,当脚本加载成功时,该 promise 将以 script 为结果 resolve,否则将以出现的 error 为结果 reject
    3. 正如我们所看到的,新的函数是对原始的 loadScript 函数的包装
      新函数调用它,并提供了自己的回调来将其转换成 promise resolve/reject

  1. 在实际开发中,我们可能需要 promise 化很多函数,所以使用一个 helper(辅助函数)很有意义
    1. 我们将其称为 promisify(f):它接受一个需要被 promise 化的函数 f,并返回一个包装(wrapper)函数。

  1. 我们可以继续改进我们的辅助函数。让我们写一个更高阶版本的 promisify
    1. 当它被以 promisify(f) 的形式调用时,它应该以与上面那个版本的实现的工作方式类似
    2. 当它被以 promisify(f, true) 的形式调用时,它应该返回以回调函数数组为结果 resolvepromise。这就是具有很多个参数的回调的结果

注意

  1. Promisification 是一种很好的方法,特别是在你使用 async/await 的时候,但不是回调的完全替代
    1. 记住,一个 promise 可能只有一个结果,但从技术上讲,一个回调可能被调用很多次
    2. 因此,promisification 仅适用于调用一次回调的函数。进一步的调用将被忽略

微任务(Microtask

概述

  1. promise 的处理程序 .then.catch.finally 都是异步的
  2. 即便一个 promise 立即被 resolve,.then.catch.finally 下面 的代码也会在这些处理程序之前被执行

  1. 如果你运行它,你会首先看到 code finished,然后才是 promise done
    1. 这很奇怪,因为这个 promise 肯定是一开始就完成的
    2. 为什么 .then 会在之后才被触发?这是怎么回事?

微任务队列(Microtask queue

  1. 异步任务需要适当的管理
    1. 为此,ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(V8 术语)
  2. 如 规范中所述
    1. 队列(queue)是先进先出的:首先进入队列的任务会首先运行
    2. 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务
  3. 或者,简单地说,当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序就会被放入队列中:但是它们不会立即被执行
    1. JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它
    2. promise 的处理程序总是会经过这个内部队列
  4. 如果有一个包含多个 .then/catch/finally 的链,那么它们中的每一个都是异步执行的
    1. 也就是说,它会首先进入队列,然后在当前代码执行完成并且先前排队的处理程序都完成时才会被执行
  5. 如果执行顺序对我们很重要该怎么办?我们怎么才能让 code finishedpromise done 之后出现呢?
    1. 很简单,只需要像下面这样使用 .then 将其放入队列:
    2. 现在代码就是按照预期执行的

未处理的 rejection

  1. 如果一个 promiseerror 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection
  2. 正常来说,如果我们预期可能会发生错误,我们会在 promise 链上添加 .catch 来处理 error

  1. 但是如果我们忘记添加 .catch,那么,微任务队列清空后,JavaScript 引擎会触发下面这事件:

  1. 如果我们迟一点再处理这个 error 会怎样?例如:
    1. 如果我们运行上面这段代码,我们会先看到 Promise Failed!,然后才是 caught
    2. 当微任务队列中的任务都完成时,才会生成 unhandledrejection:引擎会检查 promise,如果 promise 中的任意一个出现 “rejected” 状态,unhandledrejection 事件就会被触发
    3. 面这个例子中,被添加到 setTimeout 中的 .catch 也会被触发。只是会在 unhandledrejection 事件出现之后才会被触发,所以它并没有改变什么(没有发挥作用)

async/await

概述

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

async

  1. 让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:
    1. 在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise
    2. 其他值将自动被包装在一个 resolvedpromise

  1. 例如,下面这个函数返回一个结果为 1resolved promise,让我们测试一下:

  1. 我们也可以显式地返回一个 promise,结果是一样的:
    1. 所以说,async 确保了函数返回一个 promise,也会将非 promise 的值包装进去

await

  1. 语法
    1. 关键字 awaitJavaScript 引擎等待直到 promise 完成(settle)并返回结果

  1. 这里的例子就是一个 1 秒后 resolvepromise
    1. 这个函数在执行的时候,“暂停”在了 (*) 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行
    2. await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行
    3. 这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等
    4. 相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法。并且也更易于读写

  1. showAvatar() 例子,并将其改写成 async/await 的形式:

注意1

  1. 不能在普通函数中使用 await
    1. 如果我们尝试在非 async 函数中使用 await,则会报语法错误:

  1. 现代浏览器在 modules 里允许顶层的 await
    1. 在现代浏览器中,当我们处于一个 module 中时,那么在顶层使用 await 也是被允许的

  1. await 接受 “thenables
    1. promise.then 那样,await 允许我们使用 thenable 对象(那些具有可调用的 then 方法的对象)
    2. 这里的想法是,第三方对象可能不是一个 promise,但却是 promise 兼容的:如果这些对象支持 .then,那么就可以对它们使用 await

  1. Class 中的 async 方法
    1. 要声明一个 class 中的 async 方法,只需在对应方法前面加上 async 即可:
    2. 这里的含义是一样的:它确保了方法的返回值是一个 promise 并且可以在方法中使用 await

Error处理

  1. 如果一个 promise 正常 resolveawait promise 返回的就是其结果
    1. 但是如果 promisereject,它将 throw 这个 error,就像在这一行有一个 throw 语句那样

  1. 在真实开发中,promise 可能需要一点时间后才 reject。在这种情况下,在 await 抛出(throw)一个 error 之前会有一个延时
    1. 我们可以用 try..catch 来捕获上面提到的那个 error,与常规的 throw 使用的是一样的方式:

  1. 如果有 error 发生,执行控制权马上就会被移交至 catch 块。我们也可以用 try 包装多行 await 代码:

  1. 如果我们没有 try..catch,那么由异步函数 f() 的调用生成的 promise 将变为 rejected。我们可以在函数调用后面添加 .catch 来处理这个 error

注意2

  1. sync/awaitpromise.then/catch
    1. 当我们使用 async/await 时,几乎就不会用到 .then 了,因为 await 为我们处理了等待
    2. 并且我们使用常规的 try..catch 而不是 .catch
  2. async/await 可以和 Promise.all 一起使用
    1. 当我们需要同时等待多个 promise 时,我们可以用 Promise.all 把它们包装起来,然后使用 await

  1. 如果出现 error,也会正常传递,从失败了的 promise 传到 Promise.all,然后变成我们能通过使用 try..catch 在调用周围捕获到的异常(exception

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享