Skip to content

[翻译]Promise #5

@nineSean

Description

@nineSean

Promise


想象你是一个顶级歌手,你的粉丝每天都向你催促新的单曲。

为了安抚粉丝,你承诺一旦发行就会寄给他们。你给了张列表给粉丝。他们填上地址以便新歌发布所有订阅可以立即得到。并且如果发生状况以至无法发布,粉丝也能得到通知。

皆大欢喜:对于你来说不用再被催逼了;对于粉丝来说不会错过单曲。

类比真实生活事件,我们在编程中:

1."产品代码"执行需要时间。比如,加载一个远程脚本。这就对应的"单曲"。
2.当准备好时"消费代码"要得到结果。许多功能要得到结果,这就对应"粉丝"。
3.promise是一个特殊的JavaScript对象把它们连接到一起。这就对应"列表"。产品代码创造promise并把它给到它们以便订阅结果。

promise对象构造语法如下:

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});

传给new Promise的回调叫做执行者。当promise创建时,它会自动调用。它包括了"产品代码",最终将作为结果完成的。在上面类比的几个词中,执行者就是"单曲"。

得到的promise对象有几个内置属性:

  • state——初始状态是"pending",然后变为"fulfilled"或者"rejected"。
  • result——一个随意的值,初始为undefined

当执行者执行完毕,他会调用其中一个:

  • resolve(value) - 表明任务成功完成:
    • state切换为"fulfilled"。
    • result变为value
  • reject(error) - 表明发生错误:
    • state切换为rejected
    • resulterror

举个例子,把上面的内容串起来:

let promise = new Promise(function(resolve, reject) {
  // the function is executed automatically when the promise is constructed

  alert(resolve); // function () { [native code] }
  alert(reject);  // function () { [native code] }

  // after 1 second signal that the job is done with the result "done!"
  setTimeout(() => resolve("done!"), 1000);
});

运行上面的代码我们能看到2件事情:

1.执行者通过new Promise自动并且立即调用。
2.执行者接受2个参数:resolvereject-这2个函数来自Javascript引擎。不用自建,而执行者在准备就绪时要调用它们。

经过1秒钟,执行者调用resolve("done")产生以下结果:

这是成功的例子。

下面是一个执行者报错拒绝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);
});

小结:执行者通常要花时间执行任务,然后调用resolve或者reject来改变响应promise对象的状态(state)。

promise处于resolved或者rejected称为"settled",相反的是"pending"状态。

  • 只能是返回结果或者报错

执行者只能调用resolve或者rejectpromise状态变更后就是最终状态了。

resolvereject之后的调用将被忽略:

let promise = new Promise(function(resolve, reject) {
  resolve("done");

  reject(new Error("…")); // ignored
  setTimeout(() => resolve("…")); // ignored
});

执行者任务的完成只能有一个结果或者报错。在编程中,存在其它的数据结构允许许多"动态"的结果,例如流和队列。对比promises它们各有优劣。它们没被Javascript核心原生支持,并且缺乏某些promises提供的语言特征,让我们把焦点放回到promises上。

假如我们调用resolve/reject时传入更多参数-只有第一个有效,更多的参数将被忽略。

  • 使用Error对象reject

技术上来讲在调用reject是能像resolve一样传入任何类型的参数。但是推荐使用Error对象reject(或者是继承自Error对象)。原因下文将提及。

  • 立即调用resolve/reject

实际上执行者通常异步处理事情并且之后常会调用resolve/reject,但是却不必要。可以立即调用resolve或者reject,如下:

let promise = new Promise(function(resolve, reject) {
  resolve(123); // immediately give the result: 123
});

例如,当我们开始执行任务时就已经发生,然后看到所有的事情都已经完成。技术上来说这很好:我们立即就有了一个resolved promises

  • stateresult是内置的

promises对象的stateresult属性是内置的。在我们的代码中虽然不能直接访问它们,但却可以使用.then/catch方法访问,详见下文。

"消费者":".then"与".catch"

promise对象提供了"产品代码"(执行者)与"消费功能"(想接受result/error)之间的连接。"消费功能"可以通过使用promise.thenpromise.catch方法注册。

.then语法如下:

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

第一个函数的参数在promises resolved后得到结果后运行,第二个则是在rejected获取错误后运行。

举例如下:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve runs the first function in .then
promise.then(
  result => alert(result), // shows "done!" after 1 second
  error => alert(error) // doesn't run
);

rejection的例子:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// reject runs the second function in .then
promise.then(
  result => alert(result), // doesn't run
  error => alert(error) // shows "Error: Whoops!" after 1 second
);

如果我们只对成功完成感兴趣,那就提供只提供一个参数给.then

let promise = new Promise(resolve => {
  setTimeout(() => resolve("done!"), 1000);
});

promise.then(alert); // shows "done!" after 1 second

如果我们只对错误感兴趣,那么我们就用.then(null,function)或者使用别名为.catch(function)的方法:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second

.catch(f)的作用类似`.then(null,f),不过只是一个变形。

  • 确定的promises then方法会立即执行

如果promise在pending,.then/catch程序直到获取结果才处理。否则,如果 promise已经确定,将会立即执行:

// an immediately resolved promise
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done! (shows up right now)

这便利了工作——有时需要时间,有时要立即完成。这保证了不同的需求。

  • .then/catch的处理总是异步的

更准确来说,当.then/catch程序被执行时,首先进入内部的队列。Javascript引擎从队列获取程序,在当前代码完成后执行,类似于setTimeout(...,0)

换句话说,当.then(hander)将被触发,代替setTimeout(handler,0)做些事情。

在下面的例子中promise立即resolved,所以.then(alert)马上触发:alert函数进入队列并且在代码完成后立即执行。

// an immediately resolved promise
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done! (right after the current code finishes)

alert("code finished"); // this alert shows first

所以在.then后面的代码总是先执行(尽管在预完成的promises例子中)。通常这不重要,只是在某些场景中这会有问题。

下面让我们看看更多关于promises处理异步的实例。

实例:loadScript

我们已经在上个章节获得了loadScript函数(用来加载脚本的)。

下面是基于回调的变形,让我们回想一下:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error ` + src));

  document.head.append(script);
}

promises重构。

新的loadScript函数不用回调。代替的是创建并返回一个promise对象用于加载完成的处理。外面可以通过.then调用:

function loadScript(src) {
  return new Promise(function(resolve, reject) {
    let script = document.createElement('script');
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error("Script load error: " + src));

    document.head.append(script);
  });
}

用法:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js");

promise.then(
  script => alert(`${script.src} is loaded!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('One more handler to do something else!'));

我们能立即看出基于回调语法的好处:

  • Callbacks

    • 必须准备好callback当我们调用loadScript时。换句话说,在loadScript调用前必须知道该做什么的结果。
    • 只能有一个回调。
  • Promises

    • promises能让我们以很自然的顺序去用代码写东西。首先我们执行loadScript,然后.then写入在获得结果后要做的事情。
    • 我们可以使用promise调用.then任意次并且在之后的任意时间只要我们想。

所以promises已经让我们的代码更流畅和更灵活。但是还有更多美妙之处,在下一章节让我们拭目以待。

任务

再次resolve promise

let promise = new Promise(function(resolve, reject) {
  resolve(1);

  setTimeout(() => resolve(2), 1000);
});

promise.then(alert);
  • 答案
输出:1。
第二次调用`resolve`会被忽略,因为只考虑第一次的`reject/resolve`,之后将被忽略。

使用promise延迟

内建函数setTimeout使用回调。造个promise替代。

函数delay(ms)需要返回一个promisepromise将在ms毫秒后resolve以便我们使用.then。如下:

function delay(ms) {
  // your code
}

delay(3000).then(() => alert('runs after 3 seconds'));
  • 答案
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

delay(3000).then(() => alert('runs after 3 seconds'));

请注意此任务`resolve`未传参调用。`delay`后我们不用返回任何值,只是确认延迟。

promise画圈圈...

重构上一章的任务中showCircle函数,以便返回一个promise代替接受一个回调。

新的用法:

showCircle(150, 150, 100).then(div => {
  div.classList.add('message-ball');
  div.append("Hello, world!");
});

基于上一章的任务的解决方案

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions