25. 使用 Promise 进行异步编程
本章介绍如何通过 Promise 进行异步编程,特别是 ECMAScript 6 Promise API。 上一章 解释了 JavaScript 中异步编程的基础知识。 如果您在本章中遇到任何不理解的内容,可以参考上一章。
25.1. 概述
25.1.1. 链式调用 then()
25.1.2. 并行执行异步函数
25.1.3. 词汇表:Promises
25.2. 简介:Promises
25.3. 第一个例子
25.4. 理解 Promises 的三种方式
25.4.1. 概念上:调用基于 Promise 的函数是阻塞的
25.4.2. Promise 是异步传递值的容器
25.4.3. Promise 是一个事件发射器
25.5. 创建和使用 Promises
25.5.1. 生成 Promise
25.5.2. Promises 的状态
25.5.3. 使用 Promise
25.5.4. Promises 始终是异步的
25.6. 例子
25.6.1. 例子:将 fs.readFile()
Promise 化
25.6.2. 例子:将 XMLHttpRequest
Promise 化
25.6.3. 例子:延迟活动
25.6.4. 例子:Promise 超时
25.7. 创建 Promises 的其他方法
25.7.1. Promise.resolve()
25.7.2. Promise.reject()
25.8. 链式调用 Promises
25.8.1. 使用普通值解析 Q
25.8.2. 使用 thenable 解析 Q
25.8.3. 从 onRejected
解析 Q
25.8.4. 通过抛出异常拒绝 Q
25.8.5. 链式调用和错误
25.9. 常见的 Promise 链式调用错误
25.9.1. 错误:丢失 Promise 链的尾部
25.9.2. 错误:嵌套 Promises
25.9.3. 错误:创建 Promises 而不是链式调用
25.9.4. 错误:使用 then()
进行错误处理
25.10. 错误处理技巧
25.10.1. 操作错误与程序员错误
25.10.2. 在基于 Promise 的函数中处理异常
25.10.3. 扩展阅读
25.11. 组合 Promises
25.11.1. 手动分叉和合并计算
25.11.2. 通过 Promise.all()
分叉和合并计算
25.11.3. 通过 Promise.all()
实现 map()
25.11.4. 通过 Promise.race()
实现超时
25.12. 两个有用的附加 Promise 方法
25.12.1. done()
25.12.2. finally()
25.13. Node.js:将基于回调的同步函数与 Promises 一起使用
25.14. 兼容 ES6 的 Promise 库
25.15. 下一步:通过生成器使用 Promises
25.16. 深入了解 Promises:一个简单的实现
25.16.1. 独立的 Promise
25.16.2. 链式调用
25.16.3. 展平
25.16.4. 更详细的 Promise 状态
25.16.5. 异常
25.16.6. 揭示构造函数模式
25.17. Promises 的优点和局限性
25.17.1. Promises 的优点
25.17.2. Promises 并不总是最佳选择
25.18. 参考:ECMAScript 6 Promise API
25.18.1. Promise
构造函数
25.18.2. 静态 Promise
方法
25.18.3. Promise.prototype
方法
25.19. 扩展阅读
25.1 概述
Promise 是回调函数的替代方案,用于传递异步计算的结果。 它们需要异步函数的实现者付出更多努力,但为这些函数的用户提供了许多好处。
以下函数通过 Promise 异步返回结果
function
asyncFunc
()
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
···
resolve
(
result
);
···
reject
(
error
);
});
}
您可以按如下方式调用 asyncFunc()
asyncFunc
()
.
then
(
result
=>
{
···
})
.
catch
(
error
=>
{
···
});
25.1.1 链式调用 then()
then()
始终返回一个 Promise,这使您可以链式调用方法
asyncFunc1
()
.
then
(
result1
=>
{
// Use result1
return
asyncFunction2
();
// (A)
})
.
then
(
result2
=>
{
// (B)
// Use result2
})
.
catch
(
error
=>
{
// Handle errors of asyncFunc1() and asyncFunc2()
});
then()
返回的 Promise P 如何解决取决于其回调函数的操作
如果它返回一个 Promise(如 A 行所示),则该 Promise 的解决将转发给 P。 这就是为什么 B 行的回调函数可以获取 asyncFunction2
的 Promise 的解决结果。
如果它返回不同的值,则该值用于解决 P。
如果抛出异常,则 P 将以该异常被拒绝。
此外,请注意 catch()
如何处理两个异步函数调用(asyncFunction1()
和 asyncFunction2()
)的错误。 也就是说,未捕获的错误会一直传递,直到遇到错误处理程序。
25.1.2 并行执行异步函数
如果通过 then()
链式调用异步函数,则它们将按顺序执行,一次执行一个
asyncFunc1
()
.
then
(()
=>
asyncFunc2
());
如果您不这样做,而是立即调用所有函数,则它们基本上是并行执行的(在 Unix 进程术语中称为“分叉”)
asyncFunc1
();
asyncFunc2
();
Promise.all()
使您能够在所有结果都返回时收到通知(在 Unix 进程术语中称为“合并”)。 它的输入是一个 Promise 数组,输出是一个 Promise,该 Promise 将使用结果数组来完成。
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
])
.
then
(([
result1
,
result2
])
=>
{
···
})
.
catch
(
err
=>
{
// Receives first rejection among the Promises
···
});
25.1.3 词汇表:Promises
Promise API 用于异步传递结果。 *Promise 对象*(简称:Promise)是结果的占位符和容器,结果将通过该对象传递。
状态
Promise 始终处于三种互斥状态之一
在结果准备就绪之前,Promise 处于 *pending* 状态。
如果结果可用,则 Promise 处于 *fulfilled* 状态。
如果发生错误,则 Promise 处于 *rejected* 状态。
如果“事情已完成”(已完成或已拒绝),则 Promise 处于 *settled* 状态。
Promise 只会解决一次,然后保持不变。
对状态更改做出反应
*Promise 反应* 是您使用 Promise 方法 then()
注册的回调函数,用于在完成或拒绝时收到通知。
*thenable* 是一个具有 Promise 风格 then()
方法的对象。 每当 API 只想收到解决通知时,它只需要 thenable(例如,从 then()
和 catch()
返回的值;或传递给 Promise.all()
和 Promise.race()
的值)。
更改状态:有两个操作可以更改 Promise 的状态。 在您调用其中任何一个操作一次后,进一步的调用将无效。
*拒绝* Promise 意味着 Promise 变为 rejected 状态。
*解析* Promise 会产生不同的效果,具体取决于您使用的解析值
使用普通(非 thenable)值解析将完成 Promise。
使用 thenable T 解析 Promise P 意味着 P 不能再被解析,现在将遵循 T 的状态,包括其完成或拒绝值。 一旦 T 解决(或者如果 T 已经解决,则立即调用),相应的 P 反应将被调用。
25.2 简介:Promises
Promise 是一种模式,有助于处理一种特定类型的异步编程:异步返回单个结果的函数(或方法)。 接收此类结果的一种常用方法是通过回调(“回调作为延续”)
asyncFunction
(
arg1
,
arg2
,
result
=>
{
console
.
log
(
result
);
});
Promise 提供了一种更好的使用回调函数的方法:现在,异步函数返回一个 *Promise*,这是一个充当最终结果的占位符和容器的对象。 通过 Promise 方法 then()
注册的回调函数将在结果返回时收到通知
asyncFunction
(
arg1
,
arg2
)
.
then
(
result
=>
{
console
.
log
(
result
);
});
与回调作为延续相比,Promise 具有以下优点
无控制反转:与同步代码类似,基于 Promise 的函数返回结果,它们不会(直接)通过回调继续执行和控制执行。 也就是说,调用者保持控制权。
链式调用更简单:如果 then()
的回调函数返回一个 Promise(例如,调用另一个基于 Promise 的函数的结果),则 then()
返回该 Promise(这实际上是如何工作的更加复杂,将在后面解释)。 因此,您可以链式调用 then()
方法
asyncFunction1
(
a
,
b
)
.
then
(
result1
=>
{
console
.
log
(
result1
);
return
asyncFunction2
(
x
,
y
);
})
.
then
(
result2
=>
{
console
.
log
(
result2
);
});
组合异步调用(循环、映射等):更容易一些,因为您可以使用数据(Promise 对象)。
错误处理:正如我们稍后将看到的,使用 Promises 进行错误处理更简单,因为同样,没有控制反转。 此外,异常和异步错误的管理方式相同。
更清晰的签名:使用回调函数时,函数的参数是混合的;有些是函数的输入,有些是负责传递其输出的。 使用 Promises,函数签名变得更清晰;所有参数都是输入。
标准化:在 Promises 之前,有几种不兼容的处理异步结果的方法(Node.js 回调、XMLHttpRequest、IndexedDB 等)。 使用 Promises,有一个明确定义的标准:ECMAScript 6。 ES6 遵循 Promises/A+ [1] 标准。 自 ES6 以来,越来越多的 API 基于 Promises。
25.3 第一个例子
让我们看第一个例子,让您体验一下使用 Promises 是什么样的。
使用 Node.js 风格的回调函数,异步读取文件如下所示
fs
.
readFile
(
'config.json'
,
function
(
error
,
text
)
{
if
(
error
)
{
console
.
error
(
'Error while reading config file'
);
}
else
{
try
{
const
obj
=
JSON
.
parse
(
text
);
console
.
log
(
JSON
.
stringify
(
obj
,
null
,
4
));
}
catch
(
e
)
{
console
.
error
(
'Invalid JSON in file'
);
}
}
});
使用 Promises,相同的功能的使用方式如下
readFilePromisified
(
'config.json'
)
.
then
(
function
(
text
)
{
// (A)
const
obj
=
JSON
.
parse
(
text
);
console
.
log
(
JSON
.
stringify
(
obj
,
null
,
4
));
})
.
catch
(
function
(
error
)
{
// (B)
// File read error or JSON SyntaxError
console
.
error
(
'An error occurred'
,
error
);
});
仍然有回调函数,但它们是通过在结果上调用的方法提供的(then()
和 catch()
)。 B 行中的错误回调函数在两个方面很方便:首先,它是一种单一的错误处理风格(与上一个例子中的 if (error)
和 try-catch
相比)。 其次,您可以从一个位置处理 readFilePromisified()
和 A 行中回调函数的错误。
readFilePromisified()
的代码 稍后显示 。
25.4 理解 Promises 的三种方式
让我们看看理解 Promises 的三种方式。
以下代码包含一个基于 Promise 的函数 asyncFunc()
及其调用。
function
asyncFunc
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
// (A)
setTimeout
(()
=>
resolve
(
'DONE'
),
100
);
// (B)
});
}
asyncFunc
()
.
then
(
x
=>
console
.
log
(
'Result: '
+
x
));
// Output:
// Result: DONE
asyncFunc()
返回一个 Promise。 一旦异步计算的实际结果 'DONE'
准备就绪,它将通过 resolve()
(B 行)传递,resolve()
是 A 行中启动的回调函数的参数。
那么 Promise 是什么呢?
从概念上讲,调用 asyncFunc()
是一个阻塞函数调用。
Promise 既是值的容器,也是事件发射器。
25.4.1 概念上:调用基于 Promise 的函数是阻塞的
以下代码从异步函数 main()
调用 asyncFunc()
。 异步函数 是 ECMAScript 2017 的一个特性。
async
function
main
()
{
const
x
=
await
asyncFunc
();
// (A)
console
.
log
(
'Result: '
+
x
);
// (B)
// Same as:
// asyncFunc()
// .then(x => console.log('Result: '+x));
}
main
();
main()
的主体很好地表达了 *概念上* 发生的事情,即我们通常如何看待异步计算。 也就是说,asyncFunc()
是一个阻塞函数调用
A 行:等待 asyncFunc()
完成。
B 行:然后记录其结果 x
。
在 ECMAScript 6 和生成器之前,您无法挂起和恢复代码。 这就是为什么对于 Promises,您将代码恢复后发生的所有事情都放入回调函数中。 调用该回调函数与恢复代码相同。
25.4.2 Promise 是异步传递值的容器
如果一个函数返回一个 Promise,那么这个 Promise 就像一个空白容器,函数会在计算出结果后(通常情况下)将结果填充进去。你可以通过数组模拟这个过程的简单版本。
function
asyncFunc
()
{
const
blank
=
[];
setTimeout
(()
=>
blank
.
push
(
'DONE'
),
100
);
return
blank
;
}
const
blank
=
asyncFunc
();
// Wait until the value has been filled in
setTimeout
(()
=>
{
const
x
=
blank
[
0
];
// (A)
console
.
log
(
'Result: '
+
x
);
},
200
);
使用 Promise 时,你不需要通过 [0]
(如代码行 A 所示)访问最终值,而是使用 then()
方法和一个回调函数。
25.4.3 Promise 是一个事件发射器
另一种看待 Promise 的方式是将其视为一个发出事件的对象。
function
asyncFunc
()
{
const
eventEmitter
=
{
success
:
[]
};
setTimeout
(()
=>
{
// (A)
for
(
const
handler
of
eventEmitter
.
success
)
{
handler
(
'DONE'
);
}
},
100
);
return
eventEmitter
;
}
asyncFunc
()
.
success
.
push
(
x
=>
console
.
log
(
'Result: '
+
x
));
// (B)
注册事件监听器(代码行 B)可以在调用 asyncFunc()
之后完成,因为传递给 setTimeout()
的回调函数(代码行 A)是异步执行的(在这段代码执行完毕之后)。
普通的事件发射器擅长于传递多个事件,并在注册后立即开始传递。
相比之下,Promise 擅长于只传递一个值,并且内置了防止注册过晚的保护机制:Promise 的结果会被缓存,并传递给 Promise 兑现后注册的事件监听器。
25.5 创建和使用 Promise
让我们来看看如何在生产者和消费者端操作 Promise。
25.5.1 生成 Promise
作为生产者,你需要创建一个 Promise 并通过它发送结果。
const
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
// (A)
···
if
(
···
)
{
resolve
(
value
);
// success
}
else
{
reject
(
reason
);
// failure
}
});
25.5.2 Promise 的状态
一旦结果通过 Promise 传递,Promise 就会锁定该结果。这意味着每个 Promise 始终处于以下三种(互斥)状态之一:
Pending(待定):结果尚未计算出来(每个 Promise 的初始状态)。
Fulfilled(已兑现):结果已成功计算出来。
Rejected(已拒绝):计算过程中发生错误。
如果 Promise 处于已兑现或已拒绝状态,则称其为已 *兑现*(它所代表的计算已完成)。一个 Promise 只能兑现一次,并且一旦兑现就会保持该状态。后续尝试兑现操作将无效。
new Promise()
的参数(从代码行 A 开始)称为 *执行器*。
兑现:如果计算顺利完成,执行器会通过 resolve()
发送结果。这通常会兑现 Promise p
。但也有可能不会——使用 Promise q
进行兑现会导致 p
跟踪 q
:如果 q
仍处于待定状态,则 p
也处于待定状态。然而,无论 q
如何兑现,p
都会以相同的方式兑现。
拒绝:如果发生错误,执行器会通过 reject()
通知 Promise 消费者。这将始终拒绝 Promise。
如果在执行器内部抛出异常,则 p
将以该异常被拒绝。
25.5.3 消费 Promise
作为 promise
的消费者,你会通过 *反应* 被告知兑现或拒绝——你使用 then()
和 catch()
方法注册的回调函数。
promise
.
then
(
value
=>
{
/* fulfillment */
})
.
catch
(
error
=>
{
/* rejection */
});
Promise 之所以对异步函数(具有一次性结果)如此有用,是因为一旦 Promise 兑现,它就不会再改变。此外,永远不会出现竞争条件,因为无论是在 Promise 兑现之前还是之后调用 then()
或 catch()
都无关紧要。
在 Promise 兑现之前注册的反应将在 Promise 兑现后立即收到通知。
在 Promise 兑现之后注册的反应将“立即”收到缓存的兑现值(它们的调用将作为任务排队)。
请注意,catch()
只是调用 then()
的一种更方便(也是推荐的)替代方法。也就是说,以下两个调用是等效的:
promise
.
then
(
null
,
error
=>
{
/* rejection */
});
promise
.
catch
(
error
=>
{
/* rejection */
});
25.5.4 Promise 始终是异步的
Promise 库可以完全控制结果是同步(立即)还是异步(在当前延续,即当前代码段完成后)传递给 Promise 反应。但是,Promises/A+ 规范要求始终使用后一种执行模式。它通过 then()
方法的以下要求 (2.2.4)来规定这一点:
在执行上下文堆栈仅包含平台代码之前,不得调用 onFulfilled
或 onRejected
。
这意味着你的代码可以依赖于运行至完成语义(如上一章 所述),并且 Promise 链不会使其他任务无法获得处理时间。
此外,此约束还阻止你编写有时立即返回结果、有时异步返回结果的函数。这是一种反模式,因为它会使代码变得不可预测。有关更多信息,请参阅 Isaac Z. Schlueter 的“为异步设计 API ”。
25.6 示例
在深入探讨 Promise 之前,让我们在几个示例中使用到目前为止所学到的知识。
25.6.1 示例:将 fs.readFile()
Promise 化
以下代码是内置 Node.js 函数 fs.readFile()
的基于 Promise 的版本。
import
{
readFile
}
from
'fs'
;
function
readFilePromisified
(
filename
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
readFile
(
filename
,
{
encoding
:
'utf8'
},
(
error
,
data
)
=>
{
if
(
error
)
{
reject
(
error
);
}
else
{
resolve
(
data
);
}
});
});
}
readFilePromisified()
的使用方法如下:
readFilePromisified
(
process
.
argv
[
2
])
.
then
(
text
=>
{
console
.
log
(
text
);
})
.
catch
(
error
=>
{
console
.
log
(
error
);
});
25.6.2 示例:将 XMLHttpRequest
Promise 化
以下是一个基于 Promise 的函数,它通过基于事件的 XMLHttpRequest API 执行 HTTP GET 请求。
function
httpGet
(
url
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
const
request
=
new
XMLHttpRequest
();
request
.
onload
=
function
()
{
if
(
this
.
status
===
200
)
{
// Success
resolve
(
this
.
response
);
}
else
{
// Something went wrong (404 etc.)
reject
(
new
Error
(
this
.
statusText
));
}
};
request
.
onerror
=
function
()
{
reject
(
new
Error
(
'XMLHttpRequest Error: '
+
this
.
statusText
));
};
request
.
open
(
'GET'
,
url
);
request
.
send
();
});
}
以下是使用 httpGet()
的方法:
httpGet
(
'http://example.com/file.txt'
)
.
then
(
function
(
value
)
{
console
.
log
(
'Contents: '
+
value
);
},
function
(
reason
)
{
console
.
error
(
'Something went wrong'
,
reason
);
});
25.6.3 示例:延迟活动
让我们将 setTimeout()
实现为基于 Promise 的函数 delay()
(类似于 Q.delay()
)。
function
delay
(
ms
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
resolve
,
ms
);
// (A)
});
}
// Using delay():
delay
(
5000
).
then
(
function
()
{
// (B)
console
.
log
(
'5 seconds have passed!'
)
});
请注意,在代码行 A 中,我们使用零个参数调用 resolve
,这与调用 resolve(undefined)
相同。我们在代码行 B 中也不需要兑现值,只需忽略它即可。在这里,只需收到通知就足够了。
25.6.4 示例:Promise 超时
function
timeout
(
ms
,
promise
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
promise
.
then
(
resolve
);
setTimeout
(
function
()
{
reject
(
new
Error
(
'Timeout after '
+
ms
+
' ms'
));
// (A)
},
ms
);
});
}
请注意,超时后的拒绝(代码行 A)不会取消请求,但会阻止 Promise 以其结果兑现。
使用 timeout()
的方法如下:
timeout
(
5000
,
httpGet
(
'http://example.com/file.txt'
))
.
then
(
function
(
value
)
{
console
.
log
(
'Contents: '
+
value
);
})
.
catch
(
function
(
reason
)
{
console
.
error
(
'Error or timeout'
,
reason
);
});
25.7 创建 Promise 的其他方法
现在我们准备深入探讨 Promise 的特性。让我们先来探讨另外两种创建 Promise 的方法。
25.7.1 Promise.resolve()
Promise.resolve(x)
的工作原理如下:
对于大多数值 x
,它返回一个以 x
兑现的 Promise。
Promise
.
resolve
(
'abc'
)
.
then
(
x
=>
console
.
log
(
x
));
// abc
如果 x
是一个构造函数为接收器(如果调用 Promise.resolve()
则为 Promise
)的 Promise,则返回 x
本身。
const
p
=
new
Promise
(()
=>
null
);
console
.
log
(
Promise
.
resolve
(
p
)
===
p
);
// true
如果 x
是一个 thenable 对象,则将其转换为 Promise:thenable 对象的兑现也将成为 Promise 的兑现。以下代码演示了这一点。fulfilledThenable
的行为大致类似于以字符串 'hello'
兑现的 Promise。将其转换为 Promise promise
后,then()
方法按预期工作(最后一行)。
const
fulfilledThenable
=
{
then
(
reaction
)
{
reaction
(
'hello'
);
}
};
const
promise
=
Promise
.
resolve
(
fulfilledThenable
);
console
.
log
(
promise
instanceof
Promise
);
// true
promise
.
then
(
x
=>
console
.
log
(
x
));
// hello
这意味着你可以使用 Promise.resolve()
将任何值(Promise、thenable 对象或其他值)转换为 Promise。实际上,Promise.all()
和 Promise.race()
使用它将任意值的数组转换为 Promise 数组。
25.7.2 Promise.reject()
Promise.reject(err)
返回一个以 err
拒绝的 Promise。
const
myError
=
new
Error
(
'Problem!'
);
Promise
.
reject
(
myError
)
.
catch
(
err
=>
console
.
log
(
err
===
myError
));
// true
25.8 Promise 链
在本节中,我们将仔细研究如何链接 Promise。方法调用的结果
P
.
then
(
onFulfilled
,
onRejected
)
是一个新的 Promise Q。这意味着你可以通过在 Q 上调用 then()
来保持基于 Promise 的控制流。
Q 将以 onFulfilled
或 onRejected
返回的值兑现。
如果 onFulfilled
或 onRejected
抛出异常,则 Q 将被拒绝。
25.8.1 使用普通值兑现 Q
如果你使用普通值兑现 then()
返回的 Promise Q,则可以通过后续的 then()
获取该值。
asyncFunc
()
.
then
(
function
(
value1
)
{
return
123
;
})
.
then
(
function
(
value2
)
{
console
.
log
(
value2
);
// 123
});
25.8.2 使用 thenable 对象兑现 Q
你也可以使用 *thenable 对象* R 兑现 then()
返回的 Promise Q。thenable 对象是任何具有 then()
方法的对象,该方法的工作原理类似于 Promise.prototype.then()
。因此,Promise 是 thenable 对象。使用 R 兑现(例如,通过从 onFulfilled
返回它)意味着它被插入到 Q 的“后面”:R 的兑现将转发给 Q 的 onFulfilled
和 onRejected
回调函数。在某种程度上,Q 变成了 R。
此机制的主要用途是扁平化嵌套的 then()
调用,如下例所示:
asyncFunc1
()
.
then
(
function
(
value1
)
{
asyncFunc2
()
.
then
(
function
(
value2
)
{
···
});
})
扁平化版本如下所示:
asyncFunc1
()
.
then
(
function
(
value1
)
{
return
asyncFunc2
();
})
.
then
(
function
(
value2
)
{
···
})
25.8.3 从 onRejected
兑现 Q
你在错误处理程序中返回的任何内容都将成为兑现值(而不是拒绝值!)。这允许你指定在发生故障时使用的默认值。
retrieveFileName
()
.
catch
(
function
()
{
// Something went wrong, use a default value
return
'Untitled.txt'
;
})
.
then
(
function
(
fileName
)
{
···
});
25.8.4 通过抛出异常拒绝 Q
在 then()
和 catch()
的回调函数中抛出的异常将作为拒绝传递给下一个错误处理程序。
asyncFunc
()
.
then
(
function
(
value
)
{
throw
new
Error
();
})
.
catch
(
function
(
reason
)
{
// Handle error here
});
25.8.5 链和错误
可能存在一个或多个没有错误处理程序的 then()
方法调用。然后,错误将一直传递,直到遇到错误处理程序为止。
asyncFunc1
()
.
then
(
asyncFunc2
)
.
then
(
asyncFunc3
)
.
catch
(
function
(
reason
)
{
// Something went wrong above
});
25.9 常见的 Promise 链错误
25.9.1 错误:丢失 Promise 链的尾部
在以下代码中,构建了一个由两个 Promise 组成的链,但只返回了链的第一部分。结果,链的尾部丢失了。
// Don’t do this
function
foo
()
{
const
promise
=
asyncFunc
();
promise
.
then
(
result
=>
{
···
});
return
promise
;
}
可以通过返回链的尾部来解决此问题:
function
foo
()
{
const
promise
=
asyncFunc
();
return
promise
.
then
(
result
=>
{
···
});
}
如果你不需要变量 promise
,则可以进一步简化此代码:
function
foo
()
{
return
asyncFunc
()
.
then
(
result
=>
{
···
});
}
25.9.2 错误:嵌套 Promise
在以下代码中,asyncFunc2()
的调用是嵌套的:
// Don’t do this
asyncFunc1
()
.
then
(
result1
=>
{
asyncFunc2
()
.
then
(
result2
=>
{
···
});
});
解决方法是通过从第一个 then()
返回第二个 Promise 并通过第二个链接的 then()
处理它来取消嵌套此代码:
asyncFunc1
()
.
then
(
result1
=>
{
return
asyncFunc2
();
})
.
then
(
result2
=>
{
···
});
25.9.3 错误:创建 Promise 而不是链接
在以下代码中,方法 insertInto()
为其结果创建了一个新的 Promise(代码行 A):
// Don’t do this
class
Model
{
insertInto
(
db
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
// (A)
db
.
insert
(
this
.
fields
)
// (B)
.
then
(
resultCode
=>
{
this
.
notifyObservers
({
event
:
'created'
,
model
:
this
});
resolve
(
resultCode
);
// (C)
}).
catch
(
err
=>
{
reject
(
err
);
// (D)
})
});
}
···
}
如果仔细观察,你会发现结果 Promise 主要用于转发异步方法调用 db.insert()
的兑现(代码行 C)和拒绝(代码行 D)。
解决方法是不创建 Promise,而是依赖 then()
和链接:
class
Model
{
insertInto
(
db
)
{
return
db
.
insert
(
this
.
fields
)
// (A)
.
then
(
resultCode
=>
{
this
.
notifyObservers
({
event
:
'created'
,
model
:
this
});
return
resultCode
;
// (B)
});
}
···
}
说明:
我们返回 resultCode
(代码行 B),并让 then()
为我们创建 Promise。
我们返回 Promise 链(代码行 A),then()
将传递 db.insert()
生成的任何拒绝。
25.9.4 错误:使用 then()
进行错误处理
原则上,catch(cb)
是 then(null, cb)
的缩写。但是同时使用 then()
的两个参数可能会导致问题。
// Don’t do this
asyncFunc1
()
.
then
(
value
=>
{
// (A)
doSomething
();
// (B)
return
asyncFunc2
();
// (C)
},
error
=>
{
// (D)
···
});
拒绝回调(D 行)接收 asyncFunc1()
的所有拒绝,但它不接收由完成回调(A 行)创建的拒绝。例如,B 行中的同步函数调用可能会抛出异常,或者 C 行中的异步函数调用可能会产生拒绝。
因此,最好将拒绝回调移动到链式 catch()
中。
asyncFunc1
()
.
then
(
value
=>
{
doSomething
();
return
asyncFunc2
();
})
.
catch
(
error
=>
{
···
});
25.10 错误处理技巧
25.10.1 操作错误与程序员错误
在程序中,有两种错误:
操作错误 发生在正确的程序遇到需要偏离“正常”算法的异常情况时。例如,存储设备可能在程序向其写入数据时内存不足。这种错误是可以预期的。
程序员错误 发生在代码执行错误的操作时。例如,一个函数可能要求参数是一个字符串,但接收到的却是一个数字。这种错误是意外的。
25.10.1.1 操作错误:不要混淆拒绝和异常
对于操作错误,每个函数应该只支持一种错误信号方式。对于基于 Promise 的函数,这意味着不要混淆拒绝和异常,也就是说它们不应该抛出异常。
25.10.1.2 程序员错误:快速失败
对于程序员错误,可以通过抛出异常来尽快失败。
function
downloadFile
(
url
)
{
if
(
typeof
url
!==
'string'
)
{
throw
new
Error
(
'Illegal argument: '
+
url
);
}
return
new
Promise
(
···
).
}
如果这样做,则必须确保异步代码可以处理异常。我发现对于断言和理论上可以静态检查(例如,通过分析源代码的 linter)的类似情况,抛出异常是可以接受的。
25.10.2 处理基于 Promise 的函数中的异常
如果在 then()
和 catch()
的回调中抛出异常,则这不是问题,因为这两个方法会将它们转换为拒绝。
但是,如果您通过执行同步操作来启动异步函数,情况就会有所不同。
function
asyncFunc
()
{
doSomethingSync
();
// (A)
return
doSomethingAsync
()
.
then
(
result
=>
{
···
});
}
如果在 A 行中抛出异常,则整个函数都会抛出异常。这个问题有两个解决方案。
25.10.2.1 解决方案 1:返回一个被拒绝的 Promise
您可以捕获异常并将其作为被拒绝的 Promise 返回。
function
asyncFunc
()
{
try
{
doSomethingSync
();
return
doSomethingAsync
()
.
then
(
result
=>
{
···
});
}
catch
(
err
)
{
return
Promise
.
reject
(
err
);
}
}
25.10.2.2 解决方案 2:在回调中执行同步代码
您也可以通过 Promise.resolve()
启动 then()
方法调用链,并在回调中执行同步代码。
function
asyncFunc
()
{
return
Promise
.
resolve
()
.
then
(()
=>
{
doSomethingSync
();
return
doSomethingAsync
();
})
.
then
(
result
=>
{
···
});
}
另一种方法是通过 Promise 构造函数启动 Promise 链。
function
asyncFunc
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
doSomethingSync
();
resolve
(
doSomethingAsync
());
})
.
then
(
result
=>
{
···
});
}
这种方法可以为您节省一个计时周期(同步代码会立即执行),但会降低代码的规范性。
25.10.3 延伸阅读
本节的参考资料:
链式调用:
错误处理:
Joyent 的“Node.js 中的错误处理 ”。
“探索 ES6” Google 网上论坛中用户 Mörre Noseshine 的帖子 。
对询问是否可以从基于 Promise 的函数中抛出异常的推文 的反馈。
25.11 组合 Promise
组合意味着从现有部分创建新事物。我们已经遇到了 Promise 的顺序组合:给定两个 Promise P 和 Q,以下代码生成一个新的 Promise,在 P 完成后执行 Q。
请注意,这类似于同步代码的分号:同步操作 f()
和 g()
的顺序组合如下所示。
本节介绍组合 Promise 的其他方法。
25.11.1 手动分叉和连接计算
假设您要并行执行两个异步计算,asyncFunc1()
和 asyncFunc2()
。
// Don’t do this
asyncFunc1
()
.
then
(
result1
=>
{
handleSuccess
({
result1
});
});
.
catch
(
handleError
);
asyncFunc2
()
.
then
(
result2
=>
{
handleSuccess
({
result2
});
})
.
catch
(
handleError
);
const
results
=
{};
function
handleSuccess
(
props
)
{
Object
.
assign
(
results
,
props
);
if
(
Object
.
keys
(
results
).
length
===
2
)
{
const
{
result1
,
result2
}
=
results
;
···
}
}
let
errorCounter
=
0
;
function
handleError
(
err
)
{
errorCounter
++
;
if
(
errorCounter
===
1
)
{
// One error means that everything failed,
// only react to first error
···
}
}
这两个函数调用 asyncFunc1()
和 asyncFunc2()
是在没有 then()
链的情况下进行的。因此,它们都会立即执行,并且或多或少是并行的。现在执行已分叉;每个函数调用都产生了一个单独的“线程”。一旦两个线程都完成(有结果或错误),执行就会在 handleSuccess()
或 handleError()
中连接到单个线程。
这种方法的问题在于它涉及太多手动且容易出错的工作。解决方法是不要自己做这件事,而是依靠内置方法 Promise.all()
。
25.11.2 通过 Promise.all()
分叉和连接计算
Promise.all(iterable)
接受一个 Promise 的可迭代对象(thenable 和其他值通过 Promise.resolve()
转换为 Promise)。一旦所有 Promise 都已完成,它就会使用其值的数组来完成。如果 iterable
为空,则 all()
返回的 Promise 会立即完成。
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
])
.
then
(([
result1
,
result2
])
=>
{
···
})
.
catch
(
err
=>
{
// Receives first rejection among the Promises
···
});
25.11.3 通过 Promise.all()
实现 map()
Promise 的一个好处是,许多同步工具仍然有效,因为基于 Promise 的函数会返回结果。例如,您可以使用数组方法 map()
。
const
fileUrls
=
[
'http://example.com/file1.txt'
,
'http://example.com/file2.txt'
,
];
const
promisedTexts
=
fileUrls
.
map
(
httpGet
);
promisedTexts
是一个 Promise 数组。我们可以使用上一节中已经介绍过的 Promise.all()
将该数组转换为一个 Promise,该 Promise 使用结果数组来完成。
Promise
.
all
(
promisedTexts
)
.
then
(
texts
=>
{
for
(
const
text
of
texts
)
{
console
.
log
(
text
);
}
})
.
catch
(
reason
=>
{
// Receives first rejection among the Promises
});
25.11.4 通过 Promise.race()
实现超时
Promise.race(iterable)
接受一个 Promise 的可迭代对象(thenable 和其他值通过 Promise.resolve()
转换为 Promise),并返回一个 Promise P。第一个完成的输入 Promise 会将其完成状态传递给输出 Promise。如果 iterable
为空,则 race()
返回的 Promise 永远不会完成。
例如,让我们使用 Promise.race()
来实现超时。
Promise
.
race
([
httpGet
(
'http://example.com/file.txt'
),
delay
(
5000
).
then
(
function
()
{
throw
new
Error
(
'Timed out'
)
});
])
.
then
(
function
(
text
)
{
···
})
.
catch
(
function
(
reason
)
{
···
});
25.12 两个有用的附加 Promise 方法
本节介绍许多 Promise 库提供的两个有用的 Promise 方法。它们只是为了进一步演示 Promise,您不应该将它们添加到 Promise.prototype
中(这种类型的修补应该只由 polyfill 完成)。
25.12.1 done()
当您链接多个 Promise 方法调用时,您可能会冒着静默丢弃错误的风险。例如:
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
then
(
f2
);
// (A)
}
如果 A 行中的 then()
产生拒绝,则它永远不会在任何地方被处理。Promise 库 Q 提供了一个方法 done()
,用作方法调用链中的最后一个元素。它要么替换最后一个 then()
(并有一个或两个参数):
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
done
(
f2
);
}
要么插入到最后一个 then()
之后(并且没有参数):
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
then
(
f2
)
.
done
();
}
引用Q 文档 :
done
与 then
使用的黄金法则是:要么将您的 Promise 返回给其他人,要么如果链条以您结束,则调用 done
来终止它。使用 catch
终止是不够的,因为 catch 处理程序本身可能会抛出错误。
这就是您在 ECMAScript 6 中实现 done()
的方式:
Promise
.
prototype
.
done
=
function
(
onFulfilled
,
onRejected
)
{
this
.
then
(
onFulfilled
,
onRejected
)
.
catch
(
function
(
reason
)
{
// Throw an exception globally
setTimeout
(()
=>
{
throw
reason
},
0
);
});
};
虽然 done
的功能显然很有用,但它还没有被添加到 ECMAScript 6 中。其想法是首先探索引擎可以自动检测到多少。根据其工作情况,可能需要引入 done()
。
25.12.2 finally()
有时,您希望执行一个操作,而不管是否发生错误。例如,在您使用完资源后进行清理。这就是 Promise 方法 finally()
的用途,它的工作方式与异常处理中的 finally
子句非常相似。它的回调不接收任何参数,但会被通知完成或拒绝。
createResource
(
···
)
.
then
(
function
(
value1
)
{
// Use resource
})
.
then
(
function
(
value2
)
{
// Use resource
})
.
finally
(
function
()
{
// Clean up
});
这就是 Domenic Denicola 建议 实现 finally()
的方式:
Promise
.
prototype
.
finally
=
function
(
callback
)
{
const
P
=
this
.
constructor
;
// We don’t invoke the callback in here,
// because we want then() to handle its exceptions
return
this
.
then
(
// Callback fulfills => continue with receiver’s fulfillment or rejec\
tion
// Callback rejects => pass on that rejection (then() has no 2nd para\
meter
!
)
value
=>
P
.
resolve
(
callback
()).
then
(()
=>
value
),
reason
=>
P
.
resolve
(
callback
()).
then
(()
=>
{
throw
reason
})
);
};
回调确定如何处理接收方(this
)的完成状态:
如果回调抛出异常或返回一个被拒绝的 Promise,则该异常/拒绝值将成为拒绝值。
否则,接收方的完成状态(完成或拒绝)将成为 finally()
返回的 Promise 的完成状态。在某种程度上,我们将 finally()
从方法链中移除了。
**示例 1**(作者:Jake Archibald ):使用 finally()
隐藏微调器。简化版本:
showSpinner
();
fetchGalleryData
()
.
then
(
data
=>
updateGallery
(
data
))
.
catch
(
showNoDataError
)
.
finally
(
hideSpinner
);
**示例 2**(作者:Kris Kowal ):使用 finally()
拆除测试。
const
HTTP
=
require
(
"q-io/http"
);
const
server
=
HTTP
.
Server
(
app
);
return
server
.
listen
(
0
)
.
then
(
function
()
{
// run test
})
.
finally
(
server
.
stop
);
25.13 Node.js:将基于回调的同步函数与 Promise 一起使用
Promise 库 Q 具有用于与 Node.js 风格的 (err, result)
回调 API 交互的工具函数 。例如,denodeify
将基于回调的函数转换为基于 Promise 的函数。
const
readFile
=
Q
.
denodeify
(
FS
.
readFile
);
readFile
(
'foo.txt'
,
'utf-8'
)
.
then
(
function
(
text
)
{
···
});
denodify 是一个只提供 Q.denodeify()
功能并符合 ECMAScript 6 Promise API 的微型库。
25.14 兼容 ES6 的 Promise 库
市面上有很多 Promise 库。以下库符合 ECMAScript 6 API,这意味着您可以现在就使用它们,并在以后轻松迁移到原生 ES6。
最小 polyfill:
Jake Archibald 的“ES6-Promises ”仅从 RSVP.js 中提取 ES6 API。
Kyle Simpson 的“Native Promise Only (NPO) ”是“一个用于原生 ES6 Promise 的 polyfill,尽可能接近(没有扩展)严格的规范定义”。
Calvin Metcalf 的“Lie ”是一个“小型、高性能的 Promise 库,实现了 Promises/A+ 规范”。
更大的 Promise 库:
Stefan Penner 的“RSVP.js ”是 ES6 Promise API 的超集。
Petka Antonov 的“Bluebird ”是一个流行的 Promise 库,它通过了 ES2015 测试 (Test262),因此是 ES6 Promise 的替代品。
Kris Kowal 的Q.Promise
实现了 ES6 API。
ES6 标准库 polyfill:
Paul Millr 的“ES6 Shim ”包含 Promise
。
Denis Pushkarev 的“core-js ”(Babel 使用的 ES6+ polyfill)包含 Promise
。
25.15 下一步:通过生成器使用 Promise
通过 Promise 实现异步函数比通过事件或回调更方便,但它仍然不理想。
异步代码和同步代码的工作方式完全不同。因此,混合使用这些执行样式并在函数或方法之间切换它们很麻烦。
从概念上讲,调用异步函数是一个阻塞调用:进行调用的代码在异步计算期间被挂起,并在结果出现后恢复。但是,代码并没有尽可能地反映这一点。
解决方案是将阻塞调用引入 JavaScript。生成器让我们可以通过库来做到这一点:在下面的代码中,我使用 控制流库 co 来异步检索两个 JSON 文件。
co
(
function
*
()
{
try
{
const
[
croftStr
,
bondStr
]
=
yield
Promise
.
all
([
// (A)
getFile
(
'http://localhost:8000/croft.json'
),
getFile
(
'http://localhost:8000/bond.json'
),
]);
const
croftJson
=
JSON
.
parse
(
croftStr
);
const
bondJson
=
JSON
.
parse
(
bondStr
);
console
.
log
(
croftJson
);
console
.
log
(
bondJson
);
}
catch
(
e
)
{
console
.
log
(
'Failure to read: '
+
e
);
}
});
在 A 行,执行通过 yield
阻塞(等待),直到 Promise.all()
的结果准备就绪。这意味着代码在执行异步操作时看起来是同步的。
详细信息在关于生成器的章节 中解释。
25.16 深入理解 Promise:一个简单的实现
在本节中,我们将从不同的角度来探讨 Promise:我们不是学习如何使用 API,而是查看它的一个简单实现。这个不同的角度极大地帮助我理解了 Promise。
Promise 实现称为 DemoPromise
。为了更容易理解,它与 API 并不完全匹配。但它已经足够接近,仍然可以让你深入了解实际实现所面临的挑战。
DemoPromise
是一个具有三个原型方法的类
DemoPromise.prototype.resolve(value)
DemoPromise.prototype.reject(reason)
DemoPromise.prototype.then(onFulfilled, onRejected)
也就是说,resolve
和 reject
是方法(而不是传递给构造函数的回调参数的函数)。
25.16.1 独立的 Promise
我们的第一个实现是一个具有最小功能的独立 Promise
你可以创建一个 Promise。
你可以解决或拒绝一个 Promise,并且你只能这样做一次。
你可以通过 then()
注册*反应*(回调)。无论 Promise 是否已经解决,它都必须独立工作。
以下是第一个实现的使用方式
const
dp
=
new
DemoPromise
();
dp
.
resolve
(
'abc'
);
dp
.
then
(
function
(
value
)
{
console
.
log
(
value
);
// abc
});
下图说明了我们的第一个 DemoPromise
的工作原理
25.16.1.1 DemoPromise.prototype.then()
让我们先检查 then()
。它必须处理两种情况
如果 Promise 仍在等待中,它会将 onFulfilled
和 onRejected
的调用排队,以便在 Promise 解决时使用。
如果 Promise 已经完成或拒绝,则可以立即调用 onFulfilled
或 onRejected
。
then
(
onFulfilled
,
onRejected
)
{
const
self
=
this
;
const
fulfilledTask
=
function
()
{
onFulfilled
(
self
.
promiseResult
);
};
const
rejectedTask
=
function
()
{
onRejected
(
self
.
promiseResult
);
};
switch
(
this
.
promiseState
)
{
case
'pending'
:
this
.
fulfillReactions
.
push
(
fulfilledTask
);
this
.
rejectReactions
.
push
(
rejectedTask
);
break
;
case
'fulfilled'
:
addToTaskQueue
(
fulfilledTask
);
break
;
case
'rejected'
:
addToTaskQueue
(
rejectedTask
);
break
;
}
}
前面的代码片段使用了以下辅助函数
function
addToTaskQueue
(
task
)
{
setTimeout
(
task
,
0
);
}
25.16.1.2 DemoPromise.prototype.resolve()
resolve()
的工作原理如下:如果 Promise 已经解决,它什么也不做(确保 Promise 只能解决一次)。否则,Promise 的状态变为 'fulfilled'
,结果缓存在 this.promiseResult
中。接下来,触发到目前为止已排队的全部完成反应。
resolve
(
value
)
{
if
(
this
.
promiseState
!==
'pending'
)
return
;
this
.
promiseState
=
'fulfilled'
;
this
.
promiseResult
=
value
;
this
.
_clearAndEnqueueReactions
(
this
.
fulfillReactions
);
return
this
;
// enable chaining
}
_clearAndEnqueueReactions
(
reactions
)
{
this
.
fulfillReactions
=
undefined
;
this
.
rejectReactions
=
undefined
;
reactions
.
map
(
addToTaskQueue
);
}
reject()
类似于 resolve()
。
25.16.2 链接
我们要实现的下一个功能是链接
then()
返回一个 Promise,该 Promise 将使用 onFulfilled
或 onRejected
返回的内容来解决。
如果缺少 onFulfilled
或 onRejected
,则它们将收到的任何内容都将传递给 then()
返回的 Promise。
显然,只有 then()
会改变
then
(
onFulfilled
,
onRejected
)
{
const
returnValue
=
new
Promise
();
// (A)
const
self
=
this
;
let
fulfilledTask
;
if
(
typeof
onFulfilled
===
'function'
)
{
fulfilledTask
=
function
()
{
const
r
=
onFulfilled
(
self
.
promiseResult
);
returnValue
.
resolve
(
r
);
// (B)
};
}
else
{
fulfilledTask
=
function
()
{
returnValue
.
resolve
(
self
.
promiseResult
);
// (C)
};
}
let
rejectedTask
;
if
(
typeof
onRejected
===
'function'
)
{
rejectedTask
=
function
()
{
const
r
=
onRejected
(
self
.
promiseResult
);
returnValue
.
resolve
(
r
);
// (D)
};
}
else
{
rejectedTask
=
function
()
{
// `onRejected` has not been provided
// => we must pass on the rejection
returnValue
.
reject
(
self
.
promiseResult
);
// (E)
};
}
···
return
returnValue
;
// (F)
}
then()
创建并返回一个新的 Promise(A 行和 F 行)。此外,fulfilledTask
和 rejectedTask
的设置方式不同:在解决后…
onFulfilled
的结果用于解决 returnValue
(B 行)。
如果缺少 onFulfilled
,我们将使用完成值来解决 returnValue
(C 行)。
onRejected
的结果用于解决(而不是拒绝!)returnValue
(D 行)。
如果缺少 onRejected
,我们将拒绝值传递给 returnValue
(E 行)。
25.16.3 扁平化
扁平化主要是为了使链接更方便:通常,从反应中返回值会将其传递给下一个 then()
。如果我们返回一个 Promise,如果它可以为我们“解包”,就像在下面的例子中那样,那就太好了
asyncFunc1
()
.
then
(
function
(
value1
)
{
return
asyncFunc2
();
// (A)
})
.
then
(
function
(
value2
)
{
// value2 is fulfillment value of asyncFunc2() Promise
console
.
log
(
value2
);
});
我们在 A 行返回了一个 Promise,并且不必在当前方法中嵌套调用 then()
,我们可以调用方法结果上的 then()
。因此:没有嵌套的 then()
,一切都保持扁平。
我们通过让 resolve()
方法进行扁平化来实现这一点
用 Promise Q 解决 Promise P 意味着 Q 的解决将转发给 P 的反应。
P 变得“锁定”在 Q 上:它不能再被解决(包括拒绝)。并且它的状态和结果始终与 Q 相同。
如果我们允许 Q 是可 thenable 的(而不仅仅是一个 Promise),我们可以使扁平化更通用。
为了实现锁定,我们引入了一个新的布尔标志 this.alreadyResolved
。一旦它为真,this
就被锁定并且不能再被解决。请注意,this
可能仍在等待中,因为它现在的状态与其锁定的 Promise 相同。
resolve
(
value
)
{
if
(
this
.
alreadyResolved
)
return
;
this
.
alreadyResolved
=
true
;
this
.
_doResolve
(
value
);
return
this
;
// enable chaining
}
实际的解决现在发生在私有方法 _doResolve()
中
_doResolve
(
value
)
{
const
self
=
this
;
// Is `value` a thenable?
if
(
typeof
value
===
'object'
&&
value
!==
null
&&
'then'
in
value
)
{
// Forward fulfillments and rejections from `value` to `this`.
// Added as a task (versus done immediately) to preserve async semant\
ics
.
addToTaskQueue
(
function
()
{
// (A)
value
.
then
(
function
onFulfilled
(
result
)
{
self
.
_doResolve
(
result
);
},
function
onRejected
(
error
)
{
self
.
_doReject
(
error
);
});
});
}
else
{
this
.
promiseState
=
'fulfilled'
;
this
.
promiseResult
=
value
;
this
.
_clearAndEnqueueReactions
(
this
.
fulfillReactions
);
}
}
扁平化在 A 行执行:如果 value
完成,我们希望 self
完成,如果 value
被拒绝,我们希望 self
被拒绝。转发通过私有方法 _doResolve
和 _doReject
进行,以绕过 alreadyResolved
的保护。
25.16.4 更详细的 Promise 状态
通过链接,Promise 的状态变得更加复杂(如 ECMAScript 6 规范的 第 25.4 节 所述)
如果你只是*使用* Promise,你通常可以采用简化的世界观并忽略锁定。最重要的状态相关概念仍然是“已解决”:如果 Promise 已完成或已拒绝,则表示已解决。Promise 解决后,它就不会再改变了(状态和完成或拒绝值)。
如果你想*实现* Promise,那么“解决”也很重要,现在更难理解了
直观地说,“已解决”意味着“不能再(直接)解决”。如果 Promise 已解决或已锁定,则表示已解决。引用规范:“未解决的 Promise 始终处于待定状态。已解决的 Promise 可能处于待定、已完成或已拒绝状态。”
解决不一定导致解决:你可以用另一个始终处于待定状态的 Promise 来解决一个 Promise。
解决现在包括拒绝(即,它更通用):你可以通过使用被拒绝的 Promise 来解决 Promise 来拒绝它。
25.16.5 异常
作为我们的最后一个功能,我们希望我们的 Promise 将用户代码中的异常作为拒绝处理。目前,“用户代码”指的是 then()
的两个回调参数。
以下摘录显示了我们如何将 onFulfilled
内部的异常转换为拒绝 - 通过在其调用周围包装一个 try-catch
(A 行)。
then
(
onFulfilled
,
onRejected
)
{
···
let
fulfilledTask
;
if
(
typeof
onFulfilled
===
'function'
)
{
fulfilledTask
=
function
()
{
try
{
const
r
=
onFulfilled
(
self
.
promiseResult
);
// (A)
returnValue
.
resolve
(
r
);
}
catch
(
e
)
{
returnValue
.
reject
(
e
);
}
};
}
else
{
fulfilledTask
=
function
()
{
returnValue
.
resolve
(
self
.
promiseResult
);
};
}
···
}
25.16.6 揭示构造函数模式
如果我们想将 DemoPromise
变成一个实际的 Promise 实现,我们仍然需要实现 揭示构造函数模式 [2] :ES6 Promise 不是通过方法解决和拒绝的,而是通过传递给*执行器*(构造函数的回调参数)的函数来解决和拒绝的。
如果执行器抛出异常,则必须拒绝“其”Promise。
25.17 Promise 的优点和局限性
25.17.1 Promise 的优点
25.17.1.1 统一异步 API
Promise 的一个重要优势是它们将越来越多地被异步浏览器 API 使用,并统一当前不同且不兼容的模式和约定。让我们来看看两个即将推出的基于 Promise 的 API。
Fetch API 是 XMLHttpRequest 的基于 Promise 的替代方案
fetch
(
url
)
.
then
(
request
=>
request
.
text
())
.
then
(
str
=>
···
)
fetch()
返回实际请求的 Promise,text()
返回内容作为字符串的 Promise。
用于以编程方式导入模块的 ECMAScript 6 API 也基于 Promise
System
.
import
(
'some_module.js'
)
.
then
(
some_module
=>
{
···
})
25.17.1.2 Promise 与事件
与事件相比,Promise 更适合处理一次性结果。无论是在计算结果之前还是之后注册结果,你都将获得结果。Promise 的这一优势是本质上的。另一方面,你不能将它们用于处理重复事件。链接是 Promise 的另一个优势,但可以将其添加到事件处理中。
25.17.1.3 Promise 与回调
与回调相比,Promise 具有更清晰的函数(或方法)签名。对于回调,参数用于输入和输出
fs
.
readFile
(
name
,
opts
?
,
(
err
,
string
|
Buffer
)
=>
void
)
对于 Promise,所有参数都用于输入
readFilePromisified
(
name
,
opts
?
)
:
Promise
<
string
|
Buffer
>
Promise 的其他优势包括
统一处理异步错误和正常异常。
更容易组合,因为你可以重用同步工具,例如 Array.prototype.map()
。
then()
和 catch()
的链接。
防止多次通知回调。一些开发环境还会警告从未处理过的拒绝。
25.17.2 Promise 并不总是最佳选择
Promise 适用于单个异步结果。它们不适合
重复事件:如果你对这些事件感兴趣,请查看 反应式编程 ,它为正常的事件处理添加了一种巧妙的链接方式。
数据流:目前正在开发 标准 来支持这些数据流。
ECMAScript 6 Promise 缺少两个有时很有用的功能
你不能取消它们。
你无法查询它们的进度(例如,在客户端用户界面中显示进度条)。
Q Promise 库 支持 后者,并且有 计划 将这两种功能添加到 Promises/A+ 中。
25.18 参考:ECMAScript 6 Promise API
本节概述了 ECMAScript 6 Promise API,如 规范 中所述。
25.18.1 Promise
构造函数
Promise 的构造函数按如下方式调用
const
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
···
});
此构造函数的回调称为*执行器*。执行器可以使用其参数来解决或拒绝新的 Promise p
resolve(x)
使用 x
解决 p
如果 x
是可 thenable 的,则其解决将转发给 p
(包括触发通过 then()
注册的反应)。
否则,p
将使用 x
完成。
reject(e)
使用值 e
(通常是 Error
的实例)拒绝 p
。
25.18.2 静态 Promise
方法
25.18.2.1 创建 Promise
以下两个静态方法创建其接收者的新实例
Promise.resolve(x)
:将任意值转换为 Promise,并意识到 Promise。
如果 x
的构造函数是接收者,则 x
不变地返回。
否则,返回一个新的接收器实例,该实例以 x
完成。
Promise.reject(reason)
:创建一个新的接收器实例,该实例以值 reason
拒绝。
25.18.2.2 组合 Promise
直观地说,静态方法 Promise.all()
和 Promise.race()
将 Promise 的可迭代对象组合成单个 Promise。 也就是说
它们接受一个可迭代对象。 可迭代对象的元素通过 this.resolve()
转换为 Promise。
它们返回一个新的 Promise。 该 Promise 是接收器的新实例。
这些方法是
Promise.all(iterable)
:返回一个 Promise,该 Promise…
如果 iterable
中的所有元素都已完成,则完成。 完成值:包含完成值的数组。
如果任何元素被拒绝,则拒绝。 拒绝值:第一个拒绝值。
Promise.race(iterable)
:iterable
中第一个已决定的元素用于决定返回的 Promise。
25.18.3 Promise.prototype
方法
25.18.3.1 Promise.prototype.then(onFulfilled, onRejected)
回调函数 onFulfilled
和 onRejected
被称为 *反应*。
如果 Promise 已经完成,则立即调用 onFulfilled
,或者一旦完成就立即调用。 同样,onRejected
会被告知拒绝。
then()
返回一个新的 Promise Q(通过接收器的构造函数的种类创建)
如果任何一个反应返回一个值,则 Q 将使用该值解析。
如果任何一个反应抛出异常,则 Q 将使用该异常拒绝。
省略的反应
如果省略了 onFulfilled
,则接收器的完成将转发到 then()
的结果。
如果省略了 onRejected
,则接收器的拒绝将转发到 then()
的结果。
省略的反应的默认值可以像这样实现
function
defaultOnFulfilled
(
x
)
{
return
x
;
}
function
defaultOnRejected
(
e
)
{
throw
e
;
}
25.18.3.2 Promise.prototype.catch(onRejected)
p.catch(onRejected)
与 p.then(null, onRejected)
相同。
25.19 延伸阅读
[1] “Promises/A+ ”,由 Brian Cavalier 和 Domenic Denicola 编辑(JavaScript Promise 的事实标准)
[2] “揭示构造函数模式 ”,作者 Domenic Denicola(Promise
构造函数使用此模式)