JavaScript Promises简介

organicfish 发布于1年前 阅读1849次
0 条评论

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

[鼓掌]

Promiss成为了JavaScript新特性!

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

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

  • 身旁的人都在欢呼,但是你却还没弄明白大家都为什么而欢呼。或许你甚至还不知道“promise”是什么。你耸耸肩,金色的纸片掉落在你的肩膀上。但是不必担心,我花了很长时间才弄明白为什么我应该关心promise。(这种情况)你或许想要从本文开始读起。

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

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

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 Fates 100遍才能回家。尽管如此,我还是得到了许多混合术语,但是以下是几个基础术语。

一个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'))``

Here, jQuery's $.ajax returns a Deferred. Since it has a then() method, Promise.resolve() can turn it into a JavaScript promise. However, sometimes deferreds pass multiple arguments to their callbacks, for example: 这里,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);
})

The difference is subtle, but extremely useful. Promise rejections skip forward to the next then() with a rejection callback (or catch() , since it's equivalent). With then(func1, func2) , func1 or func2 will be called, never both. But with then(func1).catch(func2) , both will be called if func1 rejects, as they're separate steps in the chain. Take the following: 两者差异很微妙,但是极为有用。

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!");
})

The flow above is very similar to normal JavaScript try/catch, errors that happen within a "try" go immediately to the catch() block. Here's the above as a flowchart (because I love flowcharts):

Follow the green lines for promises that fulfill, or the red for ones that reject.

JavaScript exceptions and promises

Rejections happen when a promise is explicitly rejected, but also implicitly if an error is thrown in the constructor callback:

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);
})

This means it's useful to do all your promise-related work inside the promise constructor callback, so errors are automatically caught and become rejections.

The same goes for errors thrown in then() callbacks.

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);
})

Error handling in practice

With our story and chapters, we can use catch to display an error to the user:

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';
})

If fetching story.chapterUrls[0] fails (e.g., http 500 or user is offline), it'll skip all following success callbacks, which includes the one in getJSON() which tries to parse the response as JSON, and also skips the callback that adds chapter1.html to the page. Instead it moves onto the catch callback. As a result, "Failed to show chapter" will be added to the page if any of the previous actions failed.

Like JavaScript's try/catch, the error is caught and subsequent code continues, so the spinner is always hidden, which is what we want. The above becomes a non-blocking async version of:

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'

You may want to catch() simply for logging purposes, without recovering from the error. To do this, just rethrow the error. We could do this in our getJSON() method:

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

So we've managed to fetch one chapter, but we want them all. Let's make that happen.

Parallelism and sequencing: getting the best of both

Thinking async isn't easy. If you're struggling to get off the mark, try writing the code as if it were synchronous. In this case:

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'

Try it

That works (see code )! But it's sync and locks up the browser while things download. To make this work async we use then() to make things happen one after another.

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';
})

But how can we loop through the chapter urls and fetch them in order? This doesn't work :

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

forEach isn't async-aware, so our chapters would appear in whatever order they download, which is basically how Pulp Fiction was written. This isn't Pulp Fiction, so let's fix it.

Creating a sequence

We want to turn our chapterUrls array into a sequence of promises. We can do that using 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);
  });
})

This is the first time we've seen Promise.resolve() , which creates a promise that resolves to whatever value you give it. If you pass it an instance of Promise it'll simply return it ( note: this is a change to the spec that some implementations don't yet follow). If you pass it something promise-like (has a then() method), it creates a genuine Promise that fulfills/rejects in the same way. If you pass in any other value, e.g., Promise.resolve('Hello') , it creates a promise that fulfills with that value. If you call it with no value, as above, it fulfills with "undefined".

There's also Promise.reject(val) , which creates a promise that rejects with the value you give it (or undefined).

We can tidy up the above code using 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())

This is doing the same as the previous example, but doesn't need the separate "sequence" variable. Our reduce callback is called for each item in the array. "sequence" is Promise.resolve() the first time around, but for the rest of the calls "sequence" is whatever we returned from the previous call. array.reduce is really useful for boiling an array down to a single value, which in this case is a promise.

Let's put it all together:

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';
})

Try it

And there we have it (see code ), a fully async version of the sync version. But we can do better. At the moment our page is downloading like this:

JavaScript Promises简介

Browsers are pretty good at downloading multiple things at once, so we're losing performance by downloading chapters one after the other. What we want to do is download them all at the same time, then process them when they've all arrived. Thankfully there's an API for this:

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

Promise.all takes an array of promises and creates a promise that fulfills when all of them successfully complete. You get an array of results (whatever the promises fulfilled to) in the same order as the promises you passed in.

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';
})

Try it

Depending on connection, this can be seconds faster than loading one-by-one (see code ), and it's less code than our first try. The chapters can download in whatever order, but they appear on screen in the right order.

JavaScript Promises简介

However, we can still improve perceived performance. When chapter one arrives we should add it to the page. This lets the user start reading before the rest of the chapters have arrived. When chapter three arrives, we wouldn't add it to the page because the user may not realise chapter two is missing. When chapter two arrives, we can add chapters two and three, etc etc.

To do this, we fetch JSON for all our chapters at the same time, then create a sequence to add them to the document:

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';
})

Try it

And there we go (see code ), the best of both! It takes the same amount of time to deliver all the content, but the user gets the first bit of content sooner.

JavaScript Promises简介

In this trivial example, all of the chapters arrive around the same time, but the benefit of displaying one at a time will be exaggerated with more, larger chapters.

Doing the above with Node.js-style callbacks or events is around double the code, but more importantly isn't as easy to follow. However, this isn't the end of the story for promises, when combined with other ES6 features they get even easier.

Bonus round: promises and generators

This next bit involves a whole bunch of new ES6 features, but it's not something you need to understand to use promises in your code today. Treat it like a movie trailer for some upcoming blockbuster features.

ES6 also gives us generators , which allow functions to exit at a particular point, like "return", but later resume from the same point and state, for example:

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

Notice the star before the function name, this makes it a generator. The yield keyword is our return/resume point. We can use it like this:

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

But what does this mean for promises? Well, you can use this return/resume behaviour to write async code that looks like (and is as easy to follow as) synchronous code. Don't worry too much about understanding it line-for-line, but here's a helper function that lets us use yield to wait for promises to settle:

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();
}

… which I pretty much lifted verbatim from Q , but adapted for JavaScript promises. With this, we can take our final best-case chapter example, mix it with a load of new ES6 goodness, and turn it into:

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

This works exactly as before but is so much easier to read. This works in Chrome and Opera today (see code ), and works in Microsoft Edge by going to about:flags and turning on the Enable experimental Javascript features setting. This will be enabled by default in an upcoming version.

This throws together a lot of new ES6 stuff: promises, generators, let, for-of. When we yield a promise, the spawn helper waits for the promise to resolve and returns the final value. If the promise rejects, spawn causes our yield statement to throw an exception, which we can catch with normal JavaScript try/catch. Amazingly simple async coding!

This pattern is so useful, it's coming to ES7 in the form of async functions . It's pretty much the same as above, but no need for a spawn method.

Promise API reference

All methods work in Chrome, Opera, Firefox, Microsoft Edge, and Safari unless otherwise noted. The polyfill provides the below for all browers.

Static Methods

Method summaries

Promise.resolve(promise); Returns promise (only if promise.constructor == Promise )

Promise.resolve(thenable); Make a new promise from the thenable. A thenable is promise-like in as far as it has a then() method.

Promise.resolve(obj); Make a promise that fulfills to obj . in this situation.

Promise.reject(obj); Make a promise that rejects to obj . For consistency and debugging (e.g. stack traces), obj should be an instanceof Error .

Promise.all(array); Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects. Each array item is passed to Promise.resolve , so the array can be a mixture of promise-like objects and other objects. The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value.

Promise.race(array); Make a Promise that fulfills as soon as any item fulfills, or rejects as soon as any item rejects, whichever happens first.

Note:I'm unconvinced of Promise.race 's usefulness; I'd rather have an opposite of Promise.all that only rejects if all items reject.

Constructor

Constructor

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

resolve(thenable)

Your promise will be fulfilled/rejected with the outcome of thenable

resolve(obj)

Your promise is fulfilled with obj

reject(obj)

Your promise is rejected with obj . For consistency and debugging (e.g., stack traces), obj should be an instanceof Error . Any errors thrown in the constructor callback will be implicitly passed to reject() .

Instance Methods

Instance Methods

promise.then(onFulfilled, onRejected)

onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects. Both are optional, if either/both are omitted the next onFulfilled / onRejected in the chain is called. Both callbacks have a single parameter, the fulfillment value or rejection reason. then() returns a new promise equivalent to the value you return from onFulfilled / onRejected after being passed through Promise.resolve . If an error is thrown in the callback, the returned promise rejects with that error.

promise.catch(onRejected) Sugar for promise.then(undefined, onRejected)

Many thanks to Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans, and Yutaka Hirano who proofread this and made corrections/recommendations.

Also, thanks to Mathias Bynens for updating various parts of the article.

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.

查看原文:JavaScript Promises简介

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。