趁着过年,讲讲 Promise
趁着过年,将讲 Promise
想象一下,你是一位顶级歌手,粉丝们日日夜夜都在为你即将发行的歌曲而发愁。
为了缓解压力,你答应出版后寄给他们。你给你的粉丝一个列表。他们可以填写自己的电子邮件地址,这样当歌曲可用时,所有订阅方都能立即收到。即使出了什么大问题,比如工作室着火了,你不能发布这首歌,他们还是会得到通知。
每个人都快乐:你,因为人们不再挤你了,还有粉丝,因为他们不会错过这首歌。
这是我们在编程中经常遇到的现实类比:
一个“生成代码”,做一些事情,并需要时间。例如,通过网络加载数据的一些代码。这是一个“歌手”。
一旦“生产代码”准备好了,“消费代码”就会想得到它的结果。许多函数可能需要这个结果。这些就是“粉丝”。
promise是一个特殊的JavaScript对象,它将“生产代码”和“消费代码”链接在一起。根据我们的类比:这是“订阅列表”。“生成代码”需要花费任何时间来生成承诺的结果,而“承诺”在结果准备好时使所有订阅的代码都可以使用该结果。
这种类比并不十分准确,因为JavaScript承诺比简单的订阅列表更复杂:它们有额外的特性和限制。但从一开始就很好。
promise对象的构造函数语法是:
let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});
传递给new Promise的函数称为executor。创建新承诺时,执行程序自动运行。它包含最终产生结果的生成代码。用上面的比喻:执行人就是“歌手”。
它的参数resolve和reject是JavaScript本身提供的回调函数。我们的代码只在执行器内部。
当executor获得结果时,不管是快还是晚,都没有关系,它应该调用以下其中一个回调函数:
resolve(value)—如果作业成功完成,则使用结果值。
reject(error)——如果发生了错误,error就是error对象。
总而言之:执行程序自动运行并尝试执行一项工作。当它完成尝试时,如果成功就调用resolve,如果有错误就调用reject。
新的promise构造函数返回的promise对象有以下内部属性:
状态——最初是“pending”,然后在调用resolve时更改为“completed”,在调用reject时更改为“rejected”。
result——最初未定义,然后在调用resolve(value)时更改为value,在调用reject(error)时更改为error。
因此执行人最终将promise移动到以下状态之一:
稍后我们将看到“粉丝”如何订阅这些变化。
下面是一个promise构造函数和一个简单的executor函数,它的“生成代码”需要花费时间(通过setTimeout):
let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});
运行上面的代码,我们可以看到两件事:
执行程序被自动且立即地(通过new Promise)调用。
执行器接收两个参数:resolve和reject。这些函数是由JavaScript引擎预先定义的,所以我们不需要创建它们。我们准备好了就叫他们其中一个。
在一秒钟的“处理”之后,执行程序调用resolve(“完成”)来生成结果。这会改变promise对象的状态:
这是一个成功完成工作的例子,一个“fulfilled prommise”。
下面是一个 Promise 执行人用错误 reject promise 的例子:
let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
reject(…)的调用将promise对象移动到“rejected”状态:
总而言之,执行者应该执行一项工作(通常需要花费时间),然后调用resolve或reject来更改相应promise对象的状态。
被解决或被拒绝的承诺称为“已解决”,而不是最初的“待解决”承诺。
执行程序应该只调用一个resolve或一个拒绝。任何状态的改变都是最终的。
所有进一步的resolve和reject调用都被忽略:
let promise = new Promise(function(resolve, reject) {
resolve("done");
reject(new Error("…")); // ignored
setTimeout(() => resolve("…")); // ignored
});
其思想是执行者完成的工作可能只有一个结果或一个错误。
同样,resolve/reject只期望一个参数(或none),并将忽略其他参数。
万一出了问题,遗嘱执行人应该调用reject。这可以用任何类型的参数来完成(就像resolve)。但是建议使用Error对象(或者从Error继承的对象)。这样做的理由很快就会变得显而易见。
在实践中,执行程序通常异步执行一些操作,并在一段时间后调用resolve/reject,但它并不需要这样做。我们也可以立即调用resolve或reject,像这样:
let promise = new Promise(function(resolve, reject) {
// not taking our time to do the job
resolve(123); // immediately give the result: 123
});
例如,这可能发生在当我们开始做一个工作,但然后看到所有事情都已经完成和缓存。
这很好。我们立即有了一个解决的承诺。