跳到主要内容

Promise

一,使用 Promise

  • Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。
  • 本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

博主建议:

  • 初学者要理解这个,先要理解什么是回调函数才是重点。要理解什么是回调函数,要理解下高阶函数。
  • -->什么是回调函数?
  • -->什么是高价函数?
  • -->什么是进程?
  • -->什么是多线程?
  • -->什么是高并发-->协程-->事件循环。
  • 如果你去了解一遍这些,在回来看 Promise 会有豁然开朗的感觉。当然本身 Promise 也不复杂,直接用也没啥问题。
使用回调函数写法
// 成功的回调函数
function successCallback(result) {
console.log('音频文件创建成功:' + result)
}
// 失败的回调函数
function failureCallback(error) {
console.log('音频文件创建失败:' + error)
}
createAudioFileAsync(audioSettings, successCallback, failureCallback)
使用Promise
const promise = createAudioFileAsync(audioSettings)
promise.then(successCallback, failureCallback)
使用Promise 简写
createAudioFileAsync(audioSettings).then(successCallback, failureCallback)

二,异步函数调用优点

  • 使用 Promise 也叫异步函数调用

1. 约定

不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:

  • 在本轮 事件循环 运行完成之前,回调函数是不会被调用的。
  • 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
  • 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。

Promise 很棒的一点就是链式调用(chaining)。

2. 链式调用

  • 连续执行两个或者多个异步操作是一个常见的需求,
  • 在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。(其实在 nodejs 中 这个叫中间件)
  • 我们可以通过创造一个 Promise 链来实现这种需求。
见证奇迹的时刻:then() 函数会返回一个和原来不同的新的 Promise:
const promise = doSomething()
const promise2 = promise.then(successCallback, failureCallback)
// 或者
const promise2 = doSomething().then(successCallback, failureCallback)
  • promise2 不仅表示 doSomething() 函数的完成,
  • 也代表了你传入的 successCallback 或者 failureCallback 的完成,
  • 这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,
  • 这样的话,在 promise2 上新增的回调函数会排在这个 Promise 对象的后面。

基本上,每一个 Promise 都代表了链中另一个异步过程的完成。

3. 以前的回调地狱

在过去,要想做多重的异步操作,会导致经典的回调地狱:
doSomething(function (result) {
doSomethingElse(
result,
function (newResult) {
doThirdThing(
newResult,
function (finalResult) {
console.log('Got the final result: ' + finalResult)
},
failureCallback
)
},
failureCallback
)
}, failureCallback)

4. 现在的 Promise 写法

现在,我们可以把回调绑定到返回的 Promise 上,形成一个 Promise 链:
doSomething()
.then(function (result) {
return doSomethingElse(result)
})
.then(function (newResult) {
return doThirdThing(newResult)
})
.then(function (finalResult) {
console.log('Got the final result: ' + finalResult)
})
.catch(failureCallback)
  • then 里的参数是可选的,catch(failureCallback) 是 then(null, failureCallback) 的缩略形式。
  • 如下所示,我们也可以用箭头函数来表示:
使用箭头函数写法,看起来更简洁
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`)
})
.catch(failureCallback)
  • 注意:
  • 一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。
  • 如果使用箭头函数,() => x() => { return x; } 更简洁一些,但后一种保留 return 的写法,才支持使用多个语句的链式调用。

5. Catch 的后续链式操作

new Promise((resolve, reject) => {
// 1.先执行这里
console.log('初始化')
resolve()
})
.then(() => {
// 出错啦
throw new Error('有哪里不对了')
})
.catch((err) => {
// 2.执行这里
console.log('执行「那个」' + err)
})
.then(() => {
// 3.接着链式执行
console.log('执行「这个」,无论前面发生了什么')
})

6. 错误传递

  • 通常,一遇到异常抛出,浏览器就会顺着 Promise 链寻找下一个 onRejected 失败回调函数或者由 .catch() 指定的回调函数。
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback)
  • 通常,一遇到异常抛出,浏览器就会顺着 Promise 链寻找下一个 onRejected 失败回调函数或者由 .catch() 指定的回调函数。
  • 这和以下同步代码的工作原理(执行过程)非常相似。
try {
let result = syncDoSomething()
let newResult = syncDoSomethingElse(result)
let finalResult = syncDoThirdThing(newResult)
console.log(`Got the final result: ${finalResult}`)
} catch (error) {
failureCallback(error)
}
使用ES7的语法糖 async await
async function foo() {
try {
const result = await doSomething()
const newResult = await doSomethingElse(result)
const finalResult = await doThirdThing(newResult)
console.log(`Got the final result: ${finalResult}`)
} catch (error) {
failureCallback(error)
}
}
  • 通过捕获所有的错误,甚至抛出异常和程序错误,Promise 解决了回调地狱的基本缺陷。
  • 这对于构建异步操作的基础功能而言是很有必要的。

7. Promise 拒绝事件

  • 当 Promise 被拒绝时,会有下文所述的两个事件之一被派发到全局作用域(通常而言,就是 window;
  • 如果是在 web worker 中使用的话,就是 Worker 或者其他 worker-based 接口)。
window.addEventListener(
'unhandledrejection',
(event) => {
/* 你可以在这里添加一些代码,以便检查
event.promise 中的 promise 和
event.reason 中的 rejection 原因 */
event.preventDefault()
},
false
)

8. 在旧式回调 API 中创建 Promise

  • 理想状态下,所有的异步函数都已经返回 Promise 了。

  • 但有一些 API 仍然使用旧方式来传入的成功(或者失败)的回调。典型的例子就是 setTimeout() (en-US) 函数:

  • 混用旧式回调和 Promise 可能会造成运行时序问题。

  • 如果 saySomething 函数失败了,或者包含了编程错误,那就没有办法捕获它了。这得怪 setTimeout。

  • 幸运地是,我们可以用 Promise 来封装它。

  • 最好的做法是,将这些有问题的函数封装起来,留在底层,并且永远不要再直接调用它们:

  • 幸运地是,我们可以用 Promise 来封装它。

  • 最好的做法是,将这些有问题的函数封装起来,留在底层,并且永远不要再直接调用它们:

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
wait(10000)
.then(() => saySomething('10 seconds'))
.catch(failureCallback)
  • 通常,Promise 的构造器接收一个执行函数 (executor),我们可以在这个执行函数里手动地 resolve 和 reject 一个 Promise。
  • 既然 setTimeout 并不会真的执行失败,那么我们可以在这种情况下忽略 reject。

9. 组合

  • Promise.all() 和 Promise.race() 是并行运行异步操作的两个组合式工具。
我们可以发起并行操作,然后等多个操作全部结束后进行下一步操作,如下:
Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
/* use result1, result2 and result3 */
})
可以使用一些聪明的 JavaScript 写法实现时序组合:
;[func1, func2, func3]
.reduce((p, f) => p.then(f), Promise.resolve())
.then((result3) => {
/* use result3 */
})
我们也可以写成可复用的函数形式,这在函数式编程中极为普遍:
const applyAsync = (acc, val) => acc.then(val)
const composeAsync =
(...funcs) =>
(x) =>
funcs.reduce(applyAsync, Promise.resolve(x))
在 ES7 标准中,时序组合可以通过使用 async/await 而变得更简单
let result
for (const f of [func1, func2, func3]) {
result = await f(result)
}
/* use last result (i.e. result3) */

10. 时序

  • 为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 的函数也总是会被异步调用:
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
  • 传递到 then() 中的函数被置入到一个微任务队列中,而不是立即执行,
  • 这意味着它是在 JavaScript 事件队列的所有运行时结束了,且事件队列被清空之后,才开始执行:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
wait().then(() => console.log(4))
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3))
console.log(1) // 1, 2, 3, 4

11. 嵌套

  • 简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise,因为嵌套经常会是粗心导致的。
  • 嵌套 Promise 是一种可以限制 catch 语句的作用域的控制结构写法。
  • 明确来说,嵌套的 catch 仅捕捉在其之前同时还必须是其作用域的错误,而捕捉不到在其链以外或者其嵌套域以外的错误。
  • 如果使用正确,那么可以实现高精度的错误修复。
doSomethingCritical()
.then((result) =>
doSomethingOptional()
.then((optionalResult) => doSomethingExtraNice(optionalResult))
.catch((e) => {
console.log(e.message)
})
) // 即使有异常也会忽略,继续运行;(最后会输出)
.then(() => moreCriticalStuff())
.catch((e) => console.log('Critical failure: ' + e.message)) // 没有输出
  • 这个内部的 catch 语句仅能捕获到 doSomethingOptional() 和 doSomethingExtraNice() 的失败,之后就恢复到 moreCriticalStuff() 的运行。
  • 重要提醒:如果 doSomethingCritical() 失败,这个错误仅会被最后的(外部)catch 语句捕获到。

12. 常见错误

错误示例,包含 3 个问题!
doSomething()
.then(function (result) {
doSomethingElse(result) // 没有返回 Promise 以及没有必要的嵌套 Promise
.then((newResult) => doThirdThing(newResult))
})
.then(() => doFourthThing())
// 最后,是没有使用 catch 终止 Promise 调用链,可能导致没有捕获的异常
  • 第一个错误是没有正确地将事物相连接。

  • 当我们创建新 Promise 但忘记返回它时,会发生这种情况。

  • 因此,链条被打破,或者更确切地说,我们有两个独立的链条竞争(同时在执行两个异步而非一个一个的执行)。

  • 这意味着 doFourthThing() 不会等待 doSomethingElse() 或 doThirdThing() 完成,并且将与它们并行运行,可能是无意的。

  • 单独的链也有单独的错误处理,导致未捕获的错误。

  • 第二个错误是不必要地嵌套,实现第一个错误。

  • 嵌套还限制了内部错误处理程序的范围,如果是非预期的,可能会导致未捕获的错误。

  • 其中一个变体是 Promise 构造函数反模式,它结合了 Promise 构造函数的多余使用和嵌套。

  • 第三个错误是忘记用 catch 终止链。这导致在大多数浏览器中不能终止的 Promise 链里的 rejection。

  • 一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,返回它。

  • 下面是修改后的平面化的代码:

doSomething()
.then(function (result) {
return doSomethingElse(result)
})
.then((newResult) => doThirdThing(newResult))
.then(() => doFourthThing())
.catch((error) => console.log(error))
  • 注意:() => x() => { return x; } 的简写。
  • 上述代码的写法就是具有适当错误处理的简单明确的链式写法。
  • 使用 async/await 可以解决以上大多数错误,使用 async/await 时,最常见的语法错误就是忘记了 await 关键字。

方法

Promise.all()

  • 多个 Promise 同时执行,当全部成功时返回一个成功后包含所有结果的数组 Promise,
  • 只要一个失败,就直接 catch 出来。
const promise1 = Promise.resolve(3)
const promise2 = 42
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo')
})

Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values)
})
// output: Array [3, 42, "foo"]

Promise.allSettled()

  • 多个 Promise 同时执行,不管成功失败,返回的 Promise 将被兑现,并带有描述每个 Promise 结果的对象数组。
const promise1 = Promise.resolve(3)
const promise2 = new Promise((resolve, reject) =>
setTimeout(reject, 100, 'foo')
)
const promises = [promise1, promise2]
Promise.allSettled(promises).then((results) =>
results.forEach((result) => console.log(result.status))
)
// Expected output:
// "fulfilled"
// "rejected"

Promise.any()

  • 同时执行多个 Promise,只要有任何一个 Promise 成功就返回这个 Promise 结果。
Promise.any([promise1, promise2, promise3]).then((value) => console.log(value))

Promise.prototype.catch()

  • 当 Promise 失败时,返回错误内容
const promise1 = new Promise((resolve, reject) => {
throw new Error('Uh-oh!')
})

promise1.catch((error) => {
console.error(error)
})
// output: Error: Uh-oh!

Promise.prototype.finally()

  • Promise 实例的 finally() 方法用于注册一个在 promise 敲定(兑现或拒绝)时调用的函数。
  • 它会立即返回一个等效的 Promise 对象,这可以允许你链式调用其他 promise 方法。
  • 这可以让你避免在 promise 的 then()catch() 处理器中重复编写代码。
function checkMail() {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) {
resolve('Mail has arrived')
} else {
reject(new Error('Failed to arrive'))
}
})
}
checkMail()
.then((mail) => {
console.log(mail)
})
.catch((err) => {
console.error(err)
})
.finally(() => {
console.log('Experiment completed')
})

Promise.race()

是不是? 比赛,那个 Promise 快,输入那个 Promise

  • Promise.race() 静态方法接受一个 promise 可迭代对象作为输入,并返回一个 Promise。
  • 这个返回的 promise 会随着第一个 promise 的敲定而敲定。
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one')
})

const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two')
})

Promise.race([promise1, promise2]).then((value) => {
console.log(value)
// 两者都有决心,但承诺2更快
})
// output: "two"

Promise.reject()

  • reject 拒绝
  • Promise.reject() 静态方法返回一个已拒绝(rejected)的 Promise 对象,拒绝原因为给定的参数。
function resolved(result) {
console.log('Resolved')
}
function rejected(result) {
console.error(result)
}
Promise.reject(new Error('fail')).then(resolved, rejected)
// output: Error: fail

Promise.resolve()

  • Promise.resolve() 静态方法将给定的值转换为一个 Promise。
  • 如果该值本身就是一个 Promise,那么该 Promise 将被返回;
  • 如果该值是一个 thenable 对象,Promise.resolve() 将调用其 then() 方法及其两个回调函数;
  • 否则,返回的 Promise 将会以该值兑现。
const promise1 = Promise.resolve(123)
promise1.then((value) => {
console.log(value)
// output: 123
})

Promise.prototype.then()

then 译 然后

  • Promise 实例的 then() 方法最多接受两个参数:
  • 用于 Promise 兑现和拒绝情况的回调函数。
  • 它立即返回一个等效的 Promise 对象,允许你链接到其他 Promise 方法,从而实现链式调用。
then(onFulfilled, onRejected)
const promise1 = new Promise((resolve, reject) => {
resolve('Success!')
})
promise1.then((value) => {
console.log(value)
// output: "Success!"
})