张大侠

JavaScript Promises简介

张大侠 · 2016-12-25翻译 · 954阅读 原文链接

女士们先生们,准备好迎接web开发历史上一个关键时刻吧。

[鼓掌]

Promise成为了JavaScript新特性!

(全场沸腾状,金光闪闪的纸片从天而降)

此刻你可能是以下情况中的一种:

  • 【一脸懵逼类】(感谢@御风提供传神的翻译)身旁的人都在欢呼,但是你却还没弄明白大家都为什么而欢呼。或许你甚至还不知道“promise”是什么。你耸耸肩,金色的纸片掉落在你的肩膀上。但是不必担心,我花了很长时间才弄明白为什么我应该关心promise。(这种情况)你或许想要从本文开头开始读起。

  • 【欢呼雀跃类】你打空拳!有关(官方API版本)时间问题对吗?你曾经使用过“promise”,但是困扰你的是所有的实现用了少量不同的API接口。JavaScript官方版本API是什么(什么时候出来)?(这种情况)你或许想要从术语这部分开始阅读本文。

  • 【前辈高人类】你早就知道了promise,并且会嘲笑那些像第一次了解到promise而欢呼雀跃的人们。花点时间享受一段时间的优越感,然后直接去往API参考部分吧。

Promise是什么?(原文:What's all the fuss about?)

JavaScript是单线程的,意味着两段脚本不可能同时执行,而必须一个接着一个执行。在不同浏览器之间,JavaScript和浏览器不同,能共享加载外部资源的线程。但是通常当JavaScript在处理打印、更改样式、响应用户行为(比如高亮文本、表单控件的相互影响)等多个任务时会将他们加入同一个执行队列中。当一个任务处于执行状态则会阻塞其他任务。

对一个人来说,则是习惯多线程的。能够用根手指打字,可以边开车边聊天。唯一能够起到阻塞作用的事情就是我们不得不打喷嚏,当我们打喷嚏的时候,其他事情就不得不暂缓进行了。这还挺烦人的,尤其是当你正在开车并且开口聊天的时候。同样,你当然也不希望写出像“打喷嚏”一样作用的代码吧。

你可能已经使用过事件处理和回调函数来避免这种情况发生。以下是一个事件处理示例代码:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

这样就不会像“打喷嚏”那样阻塞了。我们拿到了图片,并且添加了一些监听器,当其中一个监听器被调用的时候JavaScript便终止执行了。

不幸的是,在上面的例子中,监听的事件是有可能在我们开始监听它们之前就发生的,因此我们就会用的图片的“complete”属性来避免这个问题了。

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

但是,这样我们不能捕获到添加监听器之前的错误了。遗憾的是,DOM也并没有提供一种方法帮助解决这个问题。另外,这还仅仅只是一张图片,如果是加载多张图片的话情况将会变得更加复杂。

事件处理不总是最好的方法

事件处理很适合那些发生在同一对象上多次的事件——键盘弹起、开始触碰等。当处理这些事件的时候,你根本不用关心你添加事件监听器之前发生的事情。但是,当碰到异步成功/失败的情况,理想情况下你会想能够像下面的示例代码一样处理问题:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

而这正是promise能够做到的,并且有个更好的名字。如果HTML的image元素的“ready”方法返回一个promise,我们就可以像下面这么做:

img1.ready().then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()]).then(function() {
  // all loaded
}, function() {
  // one or more failed
});

从最基础的看,除了以下情况外,promise有点类似于事件监听器:

  • 一个promise只可能且仅succeed或者fail一次,也不能从success转换为failure或者failure转换成success。

  • 如果一个promise已经success或者fail,并且在其后添加了success和failure的回调。正确的回调总会被调用,即使事件发生的更早。

这对于异步success/failure是极其有用的,因为你能够不用关注得到结果的确切时间,更多地关注于对结果做出反应。

Promise术语

Domenic Denicola 看完这篇文章初稿后因为专业术语给我了一个“F”,并且惩罚我放学后手抄States and Fates100遍才能回家。尽管如此,我还是得到了许多混合术语,但是以下是几个基础术语。

一个promise可以是:

  • fullilled(完成态) - Promise成功状态
  • rejected(失败态) - Promise失败状态
  • pending(默认态) - Promise成功/失败之前的状态
  • settled - Promise已经成功/失败后的状态

The spec 也用了“thenable”这个词来描述一个类promise的对象,这个对象有一个“then”方法。因为这个术语会让我想起了前英格兰足球经理Terry Venables,所以我会尽量少用这个术语。

Promises的JavaScript实现

Promise已经以包的形式存在了,比如:

上面这些JavaScript的Promise都有一个共同的特点,遵循Promises/A+的标准。如果你使用过jQuery,有一个与之很类似的Deferreds。但是,Deferreds并不兼容于 Promise/A+,因此会略微有些不同而且用途不大。所以需要注意的是,jQuery也有类Promise类型,但是是Deferred的子集并且存在同样的问题。

尽管Promise实现遵循一套标准规范,但是总体的API接口还是有所不同。JavaScript实现的Promise API类似于RSVP.js。可以通过下列方式创建一个Promise:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

上面的Promise构造函数接受一个参数,即一个有着两个参数的回调函数,分别是resolve和reject。可能以异步的方式执行回调函数,如果全部运行成功则会执行resolve,否则执行reject。

类似于传统JavaScript中的“throw”,只是习惯上用的,但并不是必须的,作用是拒绝错误对象。错误对象的好处在于能够捕获追踪,使得调试工具更为有用。

使用Promise示例:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

“then()”方法接受两个参数,一个是成功状态的回调函数,另一个是失败状态的回调函数。两个参数都是可选的,因此可以只为成功/失败状态设置一个回调函数。

JavaScript Promise从把DOM当成“未来发生的事”(Futures)开始,并重新命名为“承诺”(Promises),最后将其传入JavaScript中。把他们放在JavaScript而不是DOM中处理最大好处就是能够被非浏览器的JS运行环境(例如Node.js)处理(至于如何在核心API中使用他们则是另一个问题了)。

尽管他们有着JavaScript的特性,但是DOM还是最好不要使用Promise。实际上,所有有着异步成功/失败的方法的新的DOM API都会使用Promises。在Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams等中已经用到了。

浏览器支持&增强(polyfill)

目前以下浏览器支持Promises: Chrome 32, Opera 19, Firefox 29, Safari 8 & Microsoft Edge默认支持Promises。

要了解哪些浏览器没有完全支持Promises实现的具体细节,或者想要让其他浏览器或者Node.js支持Promises,查看 the polyfill (2k 压缩)。

其他库兼容性

JavaScript Promises API 会像类Promise一样使用“then()”方法处理任何东西(或者在promise-speak sigh中的“thenable”),所以如果你使用其他库并返回一个Q Promise,这是没有问题的,在新的JavaScript Promise中也会表现很好。

我上面提到了,jQuery的Deferreds是有点没有用的。感谢你能够注意到并将其标准化为Promise,这是一件值得尽快做的事情。

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

这里,jQuery的“$.ajax”返回了一个Deffered。因为有一个“then()”方法,所以“Promise.resolve()”能够将其转换为一个JavaScript Promise。但是,有时Deferreds会像下面这样传递多个参数到它的回调函数中:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

JavaScript Promises会将除了第一个参数外全部忽略:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

很感激这通常是你所想要的,或者至少提供了一种方式可以访问想要的内容。另外,需要记住的是,jQuery没有将错误对象传递到rejections的会话。

将复杂异步代码变得更容易

  1. 开启一个微调组件,并显示加载中;
  2. 获取一些提供故事标题以及每个章节链接的JSON串;
  3. 将标题添加到页面中;
  4. 获取每个章节;
  5. 将故事内容添加到页面中;
  6. 停止微调组件。

...但是如果报错的时候还需要告诉用户。我们都会想要停止当前指向的微调器,否则它会继续执行,造成界面其他部分崩溃掉。

当然,你不会使用JavaScript来传递故事数据,“serving as HTML)”会更快一点,但是这个模式处理API的时候效率有点普通:需要获取并处理大量的数据。

首先,我们来处理从网络获取数据:

XMLHttpRequest对象Promise化

旧的API接口如果能以向后兼容的方式更新,那么就会被更新以能使用Promises。“XMLHttpRequest”是首选,但同时我们先来写一个简单的函数来实现GET请求:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

现在我们来使用这个函数:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

点击这里查看效果,查看开发工具的控制条打印的结果。现在我们可以不用手动敲入“XMLHttpRequest”就能发出HTTP请求,这种方式太好了,因为这样可以尽可能少地看见令人心烦的驼峰式“XMLHttpRequest”,我的人生会更幸福的。

链式

“then()”方法并不是终结,你可以使用链式调用“then()”方法一起一个接一个地转换值或者执行额外的异步行为。

转换值

你可以简单地通过返回一个新值来转换一个值:

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

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

我们来回顾一下这个案例:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

返回值是一个JSON串,但是我们把接受的当做是纯文本。我们可以修改我们的获取处理函数来使用JSON响应类型,但是我们在Promises中也可以这样解决:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

因为JSON.parse() 接受一个参数,并且返还一个转换后的值,我们可以简写如下:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

点击这里查看效果,查看开发工具的控制条打印的结果。实际上,我们可以很容易地创造一个getJSON() 函数:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() 仍然会返回一个promise,获取一个URL串作为参数,并解析后以JSON的形式响应。

异步行为队列

你也可以链式使用then来执行一个序列里的异步行为。

当你从一个“then()”回调函数中返回一些东西的时候,这个过程有点魔幻。如果你返回了一个值,下一个链式的then() 方法会接受这个返回值作为参数并调用。然而,如果上一个 then()返回了类似Promise的值,下一个 then()则会等待这个Promise,知道这个Promise成功或者失败。例如:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

这里我们创建了一个异步请求到story.json,这个过程会请求多个URL,然后我们会请求第一个。这就是Promise真正与简单回调模式不同的地方。

你可以创建一个简单的方法来获取章节:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

直到 getChapter被调用之前, story.json 都不会被下载。但是下一次getChapter 被调用时,我们会重新使用story Promise,因此story.json 仅会被获取一次。保证!

错误处理

正如上面看到的,then() 接受两个参数,一个是success后的回调,一个是failure后的回调(或者在Promises-speak中的fulfill和reject):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

你也可以使用 catch():

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

关于catch()没有什么特别的,它仅仅是then(undefined, func)的语法糖,但是易读性更好。需要注意的是,上面两段代码表现并不相同,后者等同于:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

两者差异很微妙,但是极为有用。Promise的rejections会直接跳过到下一个带有rejection回调的then()部分(或者catch(),因为两者是等同的)。then(func1, func2)中的func1func2 只能执行其中一个,而then(func1).catch(func2)如果func1拒绝后func1func2都会被执行,因为他们在同一链路的不同步骤,以下面代码为例:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

以上流程和普通的JavaScript try/catch流程很类似,当 "try" 出错后会立即被catch() 捕获。以下是上面的流程图(因为我热爱流程图):

绿色线条是Promises完成态,红色线条是Promises失败态。

JavaScript异常和Promises

当Promise明确拒绝的时候会出现Rejections,但是在构造回调函数中抛出的错误也会隐式出现Rejections。

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

这就意味着当处理内部含有Promise构造函数的类Promise工作时很有用,错误会自动被捕获并成为rejections。

同样在then() 回调函数中抛出的错误也会如此。

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

错误处理练习

在展现故事和章节的代码中,我们可以使用catch为用户显示错误:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

如果获取story.chapterUrls[0] 失败了(例如HTTP 500错误或者用户处于离线),他会跳过接下来所有成功的回调,包括试图解析JSON响应的getJSON(),以及将chapter1.html加载到页面的回调函数。相反,他会将执行catch回调函数。如果之前任一个行为失败的话,"Failed to show chapter" 会作为结果显示在页面中。

与JavaScript类似,错误捕获后,之后的代码会继续执行,所以微调组件总是隐藏的,这也是我们想要的效果。以上代码是下面代码的非阻塞异步版本:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

你可能简单就想要使用catch()用做记录日志,而不是从错误中恢复。你可以重新抛出错误来达到目的,我们可以在getJSON() 这么做:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

我们已经能过获取一个章节,但是我们想要获取全部。让我们来实现吧。

并行和顺序:达到两者最佳

考虑到异步并不容易。如果你在努力摆脱这些标记,尝试写一些类异步代码吧。在下面的案例中:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

尝试一下

参见代码,但是他是同步的,当浏览器正在下载时会则阻塞。可以使用 then()使其异步。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

但是我们怎样才能循环获取章节URL呢?这 没办法做到

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach 不是异步的(async-aware),所以章节会按照下载顺序显示(就像低级小说的书写顺序一样),这并不是低级小说,所以让我们来修复它:

创建一个序列

我们想要将chapterUrls数组转换为一个promises序列。可以使用then()做到:

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

我们第一次看见Promise.resolve(),这创建了一个Promise,并会解析任何值。如果传递的是一个“Promise”,那会直接返回这个“Promise”(注意:部分接口还没有实现)。如果你传递类Promise(带有then() 方法,那么他会创建一个真正的Promise,并以同样的方式完成/拒绝)。如果你传递任何其他的值,比如Promise.resolve('Hello'),他会创建一个Promise并用传递的值完成这个Promise。如果你没有传递值,那就会像上一个那样,并使用“undefined”完成这个Promise。

Promise.reject(val)同样如此,会创建一个Promise并拒绝你传递的值(或者undefined)。

我们可以使用array.reduce整理上面的代码:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

这和上一个例子实现的相同的功能,但是并不需要单独的“sequence”变量。数组中的每一项都会执行reduce回调函数。第一次"sequence"是Promise.resolve(),之后每一次"sequence"的值是上次调用的返回值。array.reduce 对于将一个数组转换为一个单一值,而这个转换过程就是一个Promise。

让我们把所有代码整合在一起:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

尝试一下

参见代码,一个同步的完全异步版本。但是我们可以做的更好。此时页面正在下载:

浏览器还是很擅长一次下载多个资源,所以我们如果一章一章下载的话会损失性能。我们想要同时把所有章节都下完,然后再处理他们。感谢有一个API可以实现:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all接受一个Promises数组并创建一个Promise,当所有Promise成功完成后完成。之后回到一个返回结果的数组,和传入Promise的顺序相同排序(无论Promise实现了什么)。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

尝试一下

相同网络连接下,这种方式会比一个个加载快几秒钟(参见代码)。无论章节以什么顺序下载,都会以正确的顺序显示在屏幕上。

然而,我们还可以增强我们感知的性能。当章节下载完我们应该将其加载在页面上。这回让我们在其他部分章节加载之前就可以开始阅读。当章节三下载完成,我们不应该将其加载到页面上,因为用户并不会意识到章节二丢失了。当章节二下载完成,我们可以加载章节二、章节三,等等。

为了能够做到这样,我们同时获取所有章节的JSON,然后创建一个序列将其添加到文档中。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence.then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

尝试一下

参见代码,两者最佳(并行和顺序)。这样花费了同样的事件来传递所有的内容,但是用户会觉得提前看到一点前面的内容。

在这个简单的例子中,所有章节会大概在同一时间下载,但是一次性显示一个章节的好处将会随着章节越多越大会变得更加明显。

使用Node.js风格的回调或者事件完成上述效果将会是大约两倍的代码量,但是更重要的是不容易掌握。然而,这并不是Promises故事的结局,当结合ES6特性,会变得更加容易。

额外环节:Promises和生成器

接下来一小部分包括ES6的新特性,但是目前在使用Promises中不需要理解。把这个看作是电影的预告片,并且将会出现大场面。

ES6也提供我们生成器,在生成器中允许函数在某个特定的点退出,就像“return”一样,但是随后会恢复到相同的点和状态,例如:

function *addGenerator() {
  var i = 0;
  while (true) {
    i += yield i;
  }
}

注意函数名前面的星号,这样就可以创建一个生成器。yield关键字是return/resume点。我们可以像下面这种方式使用:

var adder = addGenerator();
adder.next().value; // 0
adder.next(5).value; // 5
adder.next(5).value; // 10
adder.next(5).value; // 15
adder.next(50).value; // 65

但是这对Promises意味着什么呢?意味着你可以使用return或者resume行为来写异步代码,并且看起来像是同步代码(易于理解)。不要过于担心逐行理解代码,有一个帮助函数让我们可以使用 yield等待Promises解决:

function spawn(generatorFunc) {
  function continuer(verb, arg) {
    var result;
    try {
      result = generator[verb](arg);
    } catch (err) {
      return Promise.reject(err);
    }
    if (result.done) {
      return result.value;
    } else {
      return Promise.resolve(result.value).then(onFulfilled, onRejected);
    }
  }
  var generator = generatorFunc();
  var onFulfilled = continuer.bind(continuer, "next");
  var onRejected = continuer.bind(continuer, "throw");
  return onFulfilled();
}

...我偏爱lifted verbatim from Q,但是适合于JavaScript Promises。这样,我们可以得到最优的章节代码示例,并和ES6新的优势加载混合,最后代码如下:

spawn(function *() {
  try {
    // 'yield' effectively does an async wait,
    // returning the result of the promise
    let story = yield getJSON('story.json');
    addHtmlToPage(story.heading);

    // Map our array of chapter urls to
    // an array of chapter json promises.
    // This makes sure they all download parallel.
    let chapterPromises = story.chapterUrls.map(getJSON);

    for (let chapterPromise of chapterPromises) {
      // Wait for each chapter to be ready, then add it to the page
      let chapter = yield chapterPromise;
      addHtmlToPage(chapter.html);
    }

    addTextToPage("All done");
  }
  catch (err) {
    // try/catch just works, rejected promises are thrown here
    addTextToPage("Argh, broken: " + err.message);
  }
  document.querySelector('.spinner').style.display = 'none';
})

Try it

这和之前代码实现的效果一样,但是更加易读。目前在Chrome和Opera中有效,参考代码,在Microsoft Edge中也能生效(通过about:flags并转换为可用的实验性JavaScript特性设置)。并在接下来的版本中默认支持。

这段代码集合了很多新的ES6的概念:Promises,生成器,let,for-of。当yield一个Promise时,spawn helper会等待Promise解决并返回一个新的值。如果Promises拒绝,spawn会触发yield语句并抛出异常,我们能够在普通的JavaScript try/catch中捕获。多么简单的异步代码啊!

这种模式非常有用,在即将到来的ES7中以异步函数的方式出现。这和上面代码大部分相同,但是不需要 spawn 方法。

Promise API参考

除另外声明之外,所有方法在Chrome, Opera, Firefox, Microsoft Edge, Safari中均能生效。The polyfill 为所有浏览器提供一下方法。

静态方法

方法总结

Promise.resolve(promise); 返回Promise(仅promise.constructor == Promise

Promise.resolve(thenable); 接受一个thenable,并创建一个新的Promise。一个thenable是一个类Promise,并至少含有一个then()方法

Promise.resolve(obj); 创建一个Promise实现obj

Promise.reject(obj); 创建一个拒绝obj的Promise。为一致性或调试(堆栈追踪)使用, obj 应该是一个instanceof Error

Promise.all(array); 这个方法返回一个新的promise对象,该promise对象在iterable里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。每个数组项会传递到Promise.resolve,因此这个数组可以是类Promise对象或者其他对象的混合。实现值是每个顺序实现返回值的数组。拒绝值是第一个Promise拒绝值。

Promise.race(array); 创建一个Promise,一旦任一项目实现就会实现,任一项目拒绝就会拒绝。

说明:我不相信Promise.race没用,我反而认为Promise.all用处不大,因为如果所有项目拒绝仅仅只会拒绝。

构造器

Constructor

new Promise(function(resolve, reject) {});

resolve(thenable) 你的Promise会被实现或者拒绝thenable的输出

resolve(obj)

Your promise is fulfilled with obj 你的Promise会用obj实现

reject(obj) 创建一个拒绝obj的Promise。为一致性或调试(堆栈追踪)使用, obj 应该是一个instanceof Error。任一在构造器回调中抛出的错误会隐式传递给reject()

实例方法

实例方法

promise.then(onFulfilled, onRejected)

"promise"解决时onFulfilled被调用。 "promise"拒绝时onRejected被调用。 两者均可选,如果任一一个参数或者两者都忽略了

链接中的onFulfilled或者onRejected会被调用。

两个回调函数都有一个参数,实现值或拒绝值。then() 返回一个新的Promise,这个Promise等同于传递给Promise.resolveonFulfilled或者onRejected的返回值。如果一个错误在回调函数被抛出,返回的Promise会带着这个错误拒绝。

promise.catch(onRejected) promise.then(undefined, onRejected)的语法糖

非常感谢Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans, and Yutaka Hirano,为本片文章校对或者提出建议

另外,也感谢 Mathias Bynens更新这篇文章的部分章节

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License. For details, see our Site Policies. Java is a registered trademark of Oracle and/or its affiliates. 除非另作说明,本篇文章所有内容创作许可Creative Commons Attribution 3.0 License,样例代码许可 Apache 2.0 License,欲了解具体详情,参见Site Policies。Java是Oracle的一个注册商标的和/或其附属机构。

相关文章