├── .gitignore
├── LICENSE.md
├── README.md
└── README.zh.md
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | *.log
4 | .DS_Store
5 | bundle.js
6 | test.js
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2015 Matt DesLauriers
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
20 | OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # promise-cookbook
2 |
3 | This is a brief introduction to using Promises in JavaScript, primarily aimed at frontend developers.
4 |
5 | See [here](./README.zh.md) for a Chinese translation.
6 |
7 | ## contents
8 |
9 | - [intro](#intro)
10 | - [the problem](#the-problem)
11 | - [`async`](#async)
12 | - [promises](#promises)
13 | - [`new Promise()`](#new-promise)
14 | - [`.then(resolved, rejected)`](#thenresolved-rejected)
15 | - [`.catch(err)`](#catcherr)
16 | - [chaining](#chaining)
17 | - [resolving values](#resolving-values)
18 | - [`Promise.all()`](#promiseall)
19 | - [passing the buck](#passing-the-buck)
20 | - [`throw` and implicit catch](#throw-and-implicit-catch)
21 | - [common patterns](#common-patterns)
22 | - [memoization](#memoization)
23 | - [`Promise.resolve` / `Promise.reject`](#promiseresolve--promisereject)
24 | - [handling user errors](#handling-user-errors)
25 | - [`Promise` in ES2015](#promise-in-es2015)
26 | - [pitfalls](#pitfalls)
27 | - [promises in small modules](#promises-in-small-modules)
28 | - [complexity](#complexity)
29 | - [lock-in](#lock-in)
30 | - [further reading](#further-reading)
31 |
32 | ## intro
33 |
34 | A Promise is a programming construct that can reduce some of the pains of asynchronous programming. Using Promises can help produce code that is leaner, easier to maintain, and easier to build on.
35 |
36 | This lesson will mostly focus on ES6 Promise syntax, but will use [Bluebird](https://github.com/petkaantonov/bluebird) since it provides excellent error handling in the browser. The CommonJS syntax will need a bundler like [browserify](https://github.com/substack/node-browserify) or [webpack](http://webpack.github.io/). See [jam3-lesson-module-basics](https://github.com/Jam3/jam3-lesson-module-basics) for an introduction to CommonJS and browserify.
37 |
38 | ## the problem
39 |
40 | To demonstrate, let's take the problem of loading images in the browser. The following shows an implementation using a Node style (error-first) callback:
41 |
42 | ```javascript
43 | function loadImage(url, callback) {
44 | var image = new Image();
45 |
46 | image.onload = function() {
47 | callback(null, image);
48 | };
49 |
50 | image.onerror = function() {
51 | callback(new Error('Could not load image at ' + url));
52 | };
53 |
54 | image.src = url;
55 | }
56 | ```
57 |
58 | *Tip:* The above is implemented on npm as [img](https://www.npmjs.com/package/img).
59 |
60 | Loading a single image is relatively easy, and looks like this:
61 |
62 | ```js
63 | loadImage('one.png', function(err, image) {
64 | if (err) throw err;
65 | console.log('Image loaded', image);
66 | });
67 | ```
68 |
69 | However, as our application grows in complexity, so too does the code. If we were to take the same approach, but load three images, things get a little unwieldy:
70 |
71 | ```js
72 | loadImage('one.png', function(err, image1) {
73 | if (err) throw err;
74 |
75 | loadImage('two.png', function(err, image2) {
76 | if (err) throw err;
77 |
78 | loadImage('three.png', function(err, image3) {
79 | if (err) throw err;
80 |
81 | var images = [image1, image2, image3];
82 | console.log('All images loaded', images);
83 | });
84 | });
85 | });
86 | ```
87 |
88 | This tends to create a "Christmas Tree" of functions; and leads to code that is difficult to read and maintain. Further, if we wanted the images to load in parallel, it would need a [more complex solution](https://gist.github.com/mattdesl/7b2afa86481fbce87098).
89 |
90 | ## async
91 |
92 | There are numerous abstractions built around the error-first callbacks, sometimes called "errbacks."
93 |
94 | One way to solve the problem is with the [async](https://github.com/caolan/async) module:
95 |
96 | ```js
97 | var mapAsync = require('async').map;
98 |
99 | var urls = [ 'one.png', 'two.png' ];
100 | mapAsync(urls, loadImage, function(err, images) {
101 | if (err) throw err;
102 | console.log('All images loaded', images);
103 | });
104 | ```
105 |
106 | Similar abstractions exist independently on npm, such as:
107 |
108 | - [async-each](https://www.npmjs.com/package/async-each)
109 | - [async-each-series](https://www.npmjs.com/package/async-each-series)
110 | - [run-series](https://www.npmjs.com/package/run-series)
111 | - [run-waterfall](https://www.npmjs.com/package/run-waterfall)
112 | - [map-limit](https://www.npmjs.com/package/map-limit)
113 |
114 | This approach is very powerful. It's [a great fit for small modules](#promises-in-small-modules) as it does not introduce additional bloat or vendor lock-in, and does not have some of the other [pitfalls of promises](#pitfalls).
115 |
116 | However, in a larger scope, promises can provide a unified and composable structure throughout your application. They will also lay the groundwork for [ES7 async/await](https://jakearchibald.com/2014/es7-async-functions/).
117 |
118 | ## promises
119 |
120 | Let's re-implement the above with promises for our control flow. At first this may seem like more overhead, but the benefits will become clear shortly.
121 |
122 | ### `new Promise(function(resolve, reject) { ... })`
123 |
124 | Below is how the image loading function would be implemented with promises. We'll call it `loadImageAsync` to distinguish it from the earlier example.
125 |
126 | ```js
127 | var Promise = require('bluebird')
128 |
129 | function loadImageAsync(url) {
130 | return new Promise(function(resolve, reject) {
131 | var image = new Image();
132 |
133 | image.onload = function() {
134 | resolve(image);
135 | };
136 |
137 | image.onerror = function() {
138 | reject(new Error('Could not load image at ' + url));
139 | };
140 |
141 | image.src = url;
142 | });
143 | }
144 | ```
145 |
146 | The function returns a new instance of `Promise` which is *resolved* to `image` if the load succeeds, or *rejected* with a new `Error` if it fails. In our case, we `require('bluebird')` for the Promise implementation.
147 |
148 | The `Promise` constructor is typically only needed for edge cases like this, where we are converting a callback-style API into a promise-style API. In many cases it is preferable to use a `promisify` or `denodeify` utility which converts Node style (error-first) functions into their `Promise` counterpart.
149 |
150 | For example, the above becomes very concise with our [earlier `loadImage`](#the-problem) function:
151 |
152 | ```js
153 | var Promise = require('bluebird');
154 | var loadImageAsync = Promise.promisify(loadImage);
155 | ```
156 |
157 | Or with the [img](https://www.npmjs.com/package/img) module:
158 |
159 | ```js
160 | var Promise = require('bluebird');
161 | var loadImage = require('img');
162 | var loadImageAsync = Promise.promisify(loadImage);
163 | ```
164 |
165 | If you aren't using Bluebird, you can use [es6-denodeify](https://www.npmjs.com/package/es6-denodeify) for this.
166 |
167 | ### `.then(resolved, rejected)`
168 |
169 | Each `Promise` instance has a `then()` method on its prototype. This allows us to handle the result of the async task.
170 |
171 | ```js
172 | loadImageAsync('one.png')
173 | .then(function(image) {
174 | console.log('Image loaded', image);
175 | }, function(err) {
176 | console.error('Error loading image', err);
177 | });
178 | ```
179 |
180 | `then` takes two functions, either of which can be `null` or undefined. The `resolved` callback is called when the promise succeeds, and it is passed the resolved value (in this case `image`). The `rejected` callback is called when the promise fails, and it is passed the `Error` object we created earlier.
181 |
182 | ### `.catch(err)`
183 |
184 | Promises also have a `.catch(func)` to handle errors, which is the same as `.then(null, func)` but provides clearer intent.
185 |
186 | ```js
187 | loadImageAsync('one.png')
188 | .catch(function(err) {
189 | console.error('Could not load image', err);
190 | });
191 | ```
192 |
193 | ### chaining
194 |
195 | The `.then()` method *always returns a Promise*, which means it can be chained. The above could be re-written like so. If a promise is rejected, the next `catch()` or `then(null, rejected)` will be called.
196 |
197 | In the following example, if the `loadImageAsync` method is rejected, the only output to the console will be the error message.
198 |
199 | ```js
200 | loadImageAsync('one.png')
201 | .then(function(image) {
202 | console.log('Image loaded', image);
203 | return { width: image.width, height: image.height };
204 | })
205 | .then(function(size) {
206 | console.log('Image size:', size);
207 | })
208 | .catch(function(err) {
209 | console.error('Error in promise chain', err);
210 | });
211 | ```
212 |
213 | In general, you should be wary of long promise chains. They can be difficult to maintain and it would be better to split the tasks into smaller, named functions.
214 |
215 | ### resolving values
216 |
217 | Your `then()` and `catch()` callbacks can return a value to pass it along to the next method in the chain. For example, here we resolve errors to a default image:
218 |
219 | ```js
220 | loadImageAsync('one.png')
221 | .catch(function(err) {
222 | console.warn(err.message);
223 | return notFoundImage;
224 | })
225 | .then(function(image) {
226 | console.log('Resolved image', image);
227 | });
228 | ```
229 |
230 | The above code will try to load `'one.png'`, but will fall back to using `notFoundImage` if the load failed.
231 |
232 | The cool thing is, you can return a `Promise` instance, and it will be resolved before the next `.then()` is triggered. The value resolved by that promise will also get passed to the next `.then()`.
233 |
234 | ```js
235 | loadImageAsync('one.png')
236 | .catch(function(err) {
237 | console.warn(err.message);
238 | return loadImageAsync('not-found.png');
239 | })
240 | .then(function(image) {
241 | console.log('Resolved image', image);
242 | })
243 | .catch(function(err) {
244 | console.error('Could not load any images', err);
245 | });
246 | ```
247 |
248 | The above tries to load `'one.png'`, but if that fails it will then load `'not-found.png'`.
249 |
250 | ### `Promise.all()`
251 |
252 | Let's go back to our original task of loading multiple images.
253 |
254 | The `Promise.all()` method accepts an array of values or promises and returns a new `Promise` that is only resolved once *all* the promises are resolved. Here we map each URL to a new Promise using `loadImageAsync`, and then pass those promises to `all()`.
255 |
256 | ```js
257 | var urls = ['one.png', 'two.png', 'three.png'];
258 | var promises = urls.map(loadImageAsync);
259 |
260 | Promise.all(promises)
261 | .then(function(images) {
262 | console.log('All images loaded', images);
263 | })
264 | .catch(function(err) {
265 | console.error(err);
266 | });
267 | ```
268 |
269 | Finally, things are starting to look a bit cleaner.
270 |
271 | ### passing the buck
272 |
273 | You may still be wondering where promises improve on the `async` approach. The real benefits come from composing promises across your application.
274 |
275 | We can "pass the buck" by making named functions that return promises, and let errors bubble upstream. The above code would look like this:
276 |
277 | ```js
278 | function loadImages(urls) {
279 | var promises = urls.map(loadImageAsync);
280 | return Promise.all(promises);
281 | }
282 | ```
283 |
284 | A more complex example might look like this:
285 |
286 | ```js
287 | function getUserImages(user) {
288 | return loadUserData(user)
289 | .then(function(userData) {
290 | return loadImages(userData.imageUrls);
291 | });
292 | }
293 |
294 | function showUserImages(user) {
295 | return getUserImages(user)
296 | .then(renderGallery)
297 | .catch(renderEmptyGallery);
298 | }
299 |
300 | showUserImages('mattdesl')
301 | .catch(function(err) {
302 | showError(err);
303 | });
304 | ```
305 |
306 | ### `throw` and implicit catch
307 |
308 | If you `throw` inside your promise chain, the error will be impliticly caught by the underlying Promise implementation and treated as a call to `reject(err)`.
309 |
310 | In the following example, if the user has not activated their account, the promise will be rejected and the `showError` method will be called.
311 |
312 | ```js
313 | loadUser()
314 | .then(function(user) {
315 | if (!user.activated) {
316 | throw new Error('user has not activated their account');
317 | }
318 | return showUserGallery(user);
319 | })
320 | .catch(function(err) {
321 | showError(err.message);
322 | });
323 | ```
324 |
325 | This part of the specification is often viewed as a pitfall of promises. It conflates the semantics of error handling by combining syntax errors, programmer error (e.g. invalid parameters), and connection errors into the same logic.
326 |
327 | It leads to frustrations during browser development: you might lose debugger capabilities, stack traces, and source map details.
328 |
329 | 
330 |
331 | For many developers, this is enough reason to eschew promises in favour of error-first callbacks and abstractions like [async](#async).
332 |
333 | ## common patterns
334 |
335 | ### memoization
336 |
337 | We can use `.then()` on a promise even after the asynchronous task is long complete. For example, instead of always requesting the same `'not-found.png'` image, we can cache the result of the first request and just resolve to the same `Image` object.
338 |
339 | ```js
340 | var notFound;
341 |
342 | function getNotFoundImage() {
343 | if (notFound) {
344 | return notFound;
345 | }
346 | notFound = loadImageAsync('not-found.png');
347 | return notFound;
348 | }
349 | ```
350 |
351 | This is more useful for server requests, since the browser already has a caching layer in place for image loading.
352 |
353 | ### `Promise.resolve` / `Promise.reject`
354 |
355 | The `Promise` class also provides a `resolve` and `reject` method. When called, these will return a new promise that resolves or rejects to the (optional) value given to them.
356 |
357 | For example:
358 |
359 | ```js
360 | var thumbnail = Promise.resolve(defaultThumbnail);
361 |
362 | //query the DB
363 | if (userLoggedIn) {
364 | thumbnail = loadUserThumbnail();
365 | }
366 |
367 | //add the image to the DOM when it's ready
368 | thumbnail.then(function(image) {
369 | document.body.appendChild(image);
370 | });
371 | ```
372 |
373 | Here `loadUserThumbnail` returns a `Promise` that resolves to an image. With `Promise.resolve` we can treat `thumbnail` the same even if it doesn't involve a database query.
374 |
375 | ### handling user errors
376 |
377 | Functions that return promises should *always* return promises, so the user does not need to wrap them in a `try/catch` block.
378 |
379 | Instead of throwing errors on invalid user arguments, you should return a promise that rejects with an error. [Promise.reject()](#promiseresolve--promisereject) can be convenient here.
380 |
381 | For example, using our earlier [`loadImageAsync`](#new-promise):
382 |
383 | ```js
384 | function loadImageAsync(url) {
385 | if (typeof url !== 'string') {
386 | return Promise.reject(new TypeError('must specify a string'));
387 | }
388 |
389 | return new Promise(function (resolve, reject) {
390 | /* async code */
391 | });
392 | }
393 | ```
394 |
395 | Alternatively, you could use `throw` inside the promise function:
396 |
397 | ```js
398 | function loadImageAsync(url) {
399 | return new Promise(function (resolve, reject) {
400 | if (typeof url !== 'string') {
401 | throw new TypeError('must specify a string');
402 | }
403 |
404 | /* async code */
405 | });
406 | }
407 | ```
408 |
409 | See [here](https://www.w3.org/2001/tag/doc/promises-guide#always-return-promises) for details.
410 |
411 | ### `Promise` in ES2015
412 |
413 | Although this guide uses [bluebird](https://github.com/petkaantonov/bluebird), it should work in any standard Promise implementation. For example, using [Babel](https://babeljs.io/docs/learn-es2015/#promises).
414 |
415 | Some other implementations:
416 |
417 | - [pinkie-promise](https://github.com/floatdrop/pinkie-promise)
418 | - [es6-promise](https://www.npmjs.com/package/es6-promise)
419 |
420 | For example, in Node/browserify:
421 |
422 | ```js
423 | // use native promise if it exists
424 | // otherwise fall back to polyfill
425 | var Promise = global.Promise || require('es6-promise').Promise;
426 | ```
427 |
428 | ## pitfalls
429 |
430 | In addition to the the issues mentioned in [`throw` and implicit catch](#throw-and-implicit-catch), there are some other problems to keep in mind when choosing promises. Some developers choose not to use promises for these reasons.
431 |
432 | ### promises in small modules
433 |
434 | One situation where promises are not yet a good fit is in small, self-contained [npm](https://www.npmjs.com/) modules.
435 |
436 | - Depending on `bluebird` or `es6-promise` is a form of vendor lock-in. It can be a problem for frontend developers, where bundle size is a constraint.
437 | - Expecting the native `Promise` (ES2015) constructor is also a problem, since it creates a peer dependency on these polyfills.
438 | - Mixing different promise implementations across modules may lead to subtle bugs and debugging irks.
439 |
440 | Until native Promise support is widespread, it is often easier to use Node-style callbacks and independent [async modules](#async) for control flow and smaller bundle size.
441 |
442 | Consumers can then "promisify" your API with their favourite implementation. For example, using the [xhr](https://www.npmjs.com/package/xhr) module in Bluebird might look like this:
443 |
444 | ```js
445 | var Promise = require('bluebird')
446 | var xhrAsync = Promise.promisify(require('xhr'))
447 | ```
448 |
449 | ### complexity
450 |
451 | Promises can introduce a lot of complexity and mental overhead into a codebase (evident by the need for this guide). In real-world projects, developers will often work with promise-based code without fully understanding how promises work.
452 |
453 | See Nolan Lawson's ["We Have a Problem With Promises"](http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html) for an example of this.
454 |
455 | ### lock-in
456 |
457 | Another frustration is that promises tend to work best once *everything* in your codebase is using them. In practice, you might find yourself refactoring and "promisifying" a lot of code before you can reap the benefits of promises. It also means that new code must be written with promises in mind — you are now stuck with them!
458 |
459 | ## further reading
460 |
461 | For a comprehensive list of Promise resources and small modules to avoid library lock-in, check out [Awesome Promises](https://github.com/wbinnssmith/awesome-promises).
462 |
463 | - [JavaScript Promises: There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/)
464 | - [We Have a Problem With Promises](http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html)
465 | - [You're Missing the Point of Promises](https://blog.domenic.me/youre-missing-the-point-of-promises/)
466 |
467 | ## License
468 |
469 | MIT, see [LICENSE.md](http://github.com/mattdesl/promise-cookbook/blob/master/LICENSE.md) for details.
470 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | # promise-cookbook(Promise 手册)
2 |
3 | 这是一份 JavaScript 语言叙述的 Promise 简要介绍,主要面向的读者是前端开发者。
4 |
5 | ## 目录
6 |
7 | - [简介](#简介)
8 | - [问题的提出](#问题的提出)
9 | - [`async`](#async)
10 | - [promises](#promises)
11 | - [`new Promise()`](#new-promise)
12 | - [`.then(resolved, rejected)`](#thenresolved-rejected)
13 | - [`.catch(err)`](#catcherr)
14 | - [chaining](#chaining)
15 | - [resolving values](#resolving-values)
16 | - [`Promise.all()`](#promiseall)
17 | - [passing the buck](#passing-the-buck)
18 | - [`throw` 和隐式 catch](#throw-和隐式-catch)
19 | - [常见模式](#常见模式)
20 | - [memoization](#memoization)
21 | - [`Promise.resolve` / `Promise.reject`](#promiseresolve--promisereject)
22 | - [handling user errors](#handling-user-errors)
23 | - [ES2015 中的 `Promise`](#ES2015-中的-`Promise`)
24 | - [陷阱](#陷阱)
25 | - [小模块中的 promise](#小模块中的-promise)
26 | - [复杂性](#复杂性)
27 | - [lock-in](#lock-in)
28 | - [延伸阅读](#延伸阅读)
29 |
30 | ## 简介
31 |
32 | Promise 是一种为解决异步编程痛苦而生的程序结构。使用 Promises 可以使你的代码更为精简,而且更易扩展。
33 |
34 | 本文主要着眼于 ES6 的 Promise 语法,但是会使用 [Bluebird](https://github.com/petkaantonov/bluebird),因为它在浏览器端提供了极好的错误处理功能。CommonJS 语法的程序需要类似 [browserify](https://github.com/substack/node-browserify) 或者 [webpack](http://webpack.github.io/) 这样的工具的辅助才能运行在浏览器端。要了解 CommonJS 和 browserify,请参考 [jam3-lesson-module-basics](https://github.com/Jam3/jam3-lesson-module-basics)。
35 |
36 | ## 问题的提出
37 |
38 | 我们从浏览器端载入图片的问题开始。下面的代码展示了一种 Node 风格(error-first)回调的解决方式。
39 |
40 | ```javascript
41 | function loadImage(url, callback) {
42 | var image = new Image();
43 |
44 | image.onload = function() {
45 | callback(null, image);
46 | };
47 |
48 | image.onerror = function() {
49 | callback(new Error('Could not load image at ' + url));
50 | };
51 |
52 | image.src = url;
53 | }
54 | ```
55 |
56 | *提示:* 上面的代码使用了 [img](https://www.npmjs.com/package/img) 模块。
57 |
58 | 载入单个图片相对简单:
59 |
60 | ```js
61 | loadImage('one.png', function(err, image) {
62 | if (err) throw err;
63 | console.log('Image loaded', image);
64 | });
65 | ```
66 |
67 | 然而,当我们的应用越来越复杂的时候,这么做就不合适了。如果我们使用相同的办法,载入 3 个图片,这么写就显得很不明智了:
68 |
69 | ```js
70 | loadImage('one.png', function(err, image1) {
71 | if (err) throw err;
72 |
73 | loadImage('two.png', function(err, image2) {
74 | if (err) throw err;
75 |
76 | loadImage('three.png', function(err, image3) {
77 | if (err) throw err;
78 |
79 | var images = [image1, image2, image3];
80 | console.log('All images loaded', images);
81 | });
82 | });
83 | });
84 | ```
85 |
86 | 这种层层嵌套的结构就像一颗圣诞树,这会让你的代码丧失可读性,难以维护。
87 | 而且,如果我们想要并行地载入图片,就需要一个[更复杂的解决方案](https://gist.github.com/mattdesl/7b2afa86481fbce87098).
88 |
89 | ## async
90 |
91 | 人们发明了很多种对 error-first 回调的抽象,有些叫做“errbacks”。
92 |
93 | 解决这个问题的方法之一是用 [async](https://github.com/caolan/async) 模块:
94 |
95 | ```js
96 | var mapAsync = require('async').map;
97 |
98 | var urls = [ 'one.png', 'two.png' ];
99 | mapAsync(urls, loadImage, function(err, images) {
100 | if (err) throw err;
101 | console.log('All images loaded', images);
102 | });
103 | ```
104 |
105 | npm 上还有很多相似的解决方案,比如:
106 |
107 | - [async-each](https://www.npmjs.com/package/async-each)
108 | - [async-each-series](https://www.npmjs.com/package/async-each-series)
109 | - [run-series](https://www.npmjs.com/package/run-series)
110 | - [run-waterfall](https://www.npmjs.com/package/run-waterfall)
111 | - [map-limit](https://www.npmjs.com/package/map-limit)
112 |
113 | 这种方法非常棒。这是一种合适的 [对小模块的解决方案](#小模块中的 promise),因为它没有引入额外的概念和依赖,也没有 [promise 陷阱](#陷阱)。
114 |
115 | 然而,在处理大规模应用的回调问题时,promise 可以给你的应用提供一个统一的,可组合的结构。有人已经在 [ES7 async/await](https://jakearchibald.com/2014/es7-async-functions/) 上做了些基础工作。
116 |
117 | ## promises
118 |
119 | 让我们用 promise 的方式解决上面的问题。一开始看起来似乎会多一些开销,但是马上你好看到它带来的好处。
120 |
121 | ### `new Promise()`
122 |
123 | 下面就是用 pormise 实现的图片加载函数。为区别之前的例子,我们叫它 `loadImageAsync` 。
124 |
125 | ```js
126 | var Promise = require('bluebird')
127 |
128 | function loadImageAsync(url) {
129 | return new Promise(function(resolve, reject) {
130 | var image = new Image();
131 |
132 | image.onload = function() {
133 | resolve(image);
134 | };
135 |
136 | image.onerror = function() {
137 | reject(new Error('Could not load image at ' + url));
138 | };
139 |
140 | image.src = url;
141 | });
142 | }
143 | ```
144 |
145 | 这个函数返回一个 `Promise` 的实例,会在图片加载成功时调用 *resolve*,或者加载出错时调用 *reject*,并抛出 `Error`。
146 | 在上面的例子中,使用 `require('bluebird')` 加载 bluebird 这个 Promise 实现。
147 |
148 | `Promise` 构造器仅在上面这种需要把一个回调风格的 API 转化成 Promise 风格的 API 的情况下使用。在很多情况下,我们可以使用 `promisify` 或者 `denodeify` 方法把回调风格的函数 转化成对应的 Promise 风格的。
149 |
150 | 举个例子,在原有的 [`loadImage`](#问题的提出) 函数基础上,上面的代码可以非常简洁:
151 |
152 | ```js
153 | var Promise = require('bluebird');
154 | var loadImageAsync = Promise.promisify(loadImage);
155 | ```
156 |
157 | 或者直接使用 [img](https://www.npmjs.com/package/img) 模块:
158 |
159 | ```js
160 | var Promise = require('bluebird');
161 | var loadImage = require('img');
162 | var loadImageAsync = Promise.promisify(loadImage);
163 | ```
164 |
165 | 如果你不用 Bluebird,你可以使用 [es6-denodeify](https://www.npmjs.com/package/es6-denodeify) 作为替代。
166 |
167 | ### `.then(resolved, rejected)`
168 |
169 | 每个 `Promise` 实例的原型都有一个 `then()` 方法。这可以让我们处理异步任务的结果。
170 |
171 | ```js
172 | loadImageAsync('one.png')
173 | .then(function(image) {
174 | console.log('Image loaded', image);
175 | }, function(err) {
176 | console.error('Error loading image', err);
177 | });
178 | ```
179 |
180 | `then` 有两个函数类型参数,二者中的其一可以是 `null` 或 `undefined`。
181 | `resolved` 回调函数将会在 promise 成功时被调用,并且会传递 “resolved value”(在这个例子中就是 `image`)。
182 | `rejected` 回调函数将会在 promise 失败是被调用,并且传递 `Error` 对象。
183 |
184 | ### `.catch(err)`
185 |
186 | Promises 也有一个 `.catch(func)` 用来处理错误,与 `.then(null, func)` 相似,但是意图更明确。
187 |
188 | ```js
189 | loadImageAsync('one.png')
190 | .catch(function(err) {
191 | console.error('Could not load image', err);
192 | });
193 | ```
194 |
195 | ### chaining
196 |
197 | `.then()` 方法*总是返回一个 Promise*,这意味着它可以链式地使用。
198 | 上面的代码可以像这样重写。
199 | 如果 promise 被拒绝(rejected),下一个 `catch()` 或 `then(null, rejected)` 将会被调用。
200 |
201 | 在下面的例子中,如果 `loadImageAsync` 方法被拒绝,控制台唯一的输出就会是错误信息。
202 |
203 | ```js
204 | loadImageAsync('one.png')
205 | .then(function(image) {
206 | console.log('Image loaded', image);
207 | return { width: image.width, height: image.height };
208 | })
209 | .then(function(size) {
210 | console.log('Image size:', size);
211 | })
212 | .catch(function(err) {
213 | console.error('Error in promise chain', err);
214 | });
215 | ```
216 |
217 | 一般来说,promise 链不应该过长。它们会变得很难维护,那些异步任务应该被分拆为更小的,带名字的函数。
218 |
219 | ### resolving values
220 |
221 | `then()` and `catch()` 回调可以返回一个值,传递给链中的下一个方法。举个例子,我们可以在加载出错时使用默认图片:
222 |
223 | ```js
224 | loadImageAsync('one.png')
225 | .catch(function(err) {
226 | console.warn(err.message);
227 | return notFoundImage;
228 | })
229 | .then(function(image) {
230 | console.log('Resolved image', image);
231 | });
232 | ```
233 |
234 | 上面的代码会尝试载入 `'one.png'`,但如果加载失败,就会使用 `notFoundImage`。
235 |
236 | 有个很酷的用法是,你可以返回一个 `Promise` 实例,并且它会在下一个 `.then()` 触发之前被 resolved。这个 promise 的 resolved value 也会传递给下一个 `.then()`。
237 |
238 | ```js
239 | loadImageAsync('one.png')
240 | .catch(function(err) {
241 | console.warn(err.message);
242 | return loadImageAsync('not-found.png');
243 | })
244 | .then(function(image) {
245 | console.log('Resolved image', image);
246 | })
247 | .catch(function(err) {
248 | console.error('Could not load any images', err);
249 | });
250 | ```
251 |
252 | 上面的代码尝试载入 `'one.png'`,如果载入失败就会载入 `'not-found.png'`。
253 |
254 | ### `Promise.all()`
255 |
256 | 回到最开始的载入多个图片的问题。
257 |
258 | `Promise.all()` 方法接受的参数可以是数组,或者是 promise 对象,并返回一个新的 `Promise` 对象,这个新的 `Promise` 对象只会在*所有* promise 对象进入 resolve 状态后变成 resolve 的。下面我们用 `loadImageAsync` 把每个 URL 映射为一个新的 promise 对象,然后传递给 `all()`。
259 |
260 | ```js
261 | var urls = ['one.png', 'two.png', 'three.png'];
262 | var promises = urls.map(loadImageAsync);
263 |
264 | Promise.all(promises)
265 | .then(function(images) {
266 | console.log('All images loaded', images);
267 | })
268 | .catch(function(err) {
269 | console.error(err);
270 | });
271 | ```
272 |
273 | 这样,加载多个图片的代码看起来要清晰一些了。
274 |
275 | ### passing the buck
276 |
277 | 你可能想知道 promise 风格的解决方法与 `async` 方式相比优势在哪。当你需要组合多个 promise 时,你就能体会它的优势了。
278 |
279 | 我们可以声明多个返回 promise 的具名函数,并且让错误信息冒泡到上层函数。上面的代码可以改写为这样:
280 |
281 | ```js
282 | function loadImages(urls) {
283 | var promises = urls.map(loadImageAsync);
284 | return Promise.all(promises);
285 | }
286 | ```
287 |
288 | 更复杂的例子会像这样:
289 |
290 | ```js
291 | function getUserImages(user) {
292 | return loadUserData(user)
293 | .then(function(userData) {
294 | return loadImages(userData.imageUrls);
295 | });
296 | }
297 |
298 | function showUserImages(user) {
299 | return getUserImages(user)
300 | .then(renderGallery)
301 | .catch(renderEmptyGallery);
302 | }
303 |
304 | showUserImages('mattdesl')
305 | .catch(function(err) {
306 | showError(err);
307 | });
308 | ```
309 |
310 | ### `throw` 和隐式 catch
311 |
312 | 如果在 promise 链中 `throw` ,错误会被 Promise 底层代码隐式地 catch,并调用 `reject(err)`。
313 |
314 | 在下面的例子中,如果用户没有激活他的账号,promise 将会被 rejected,并且 `showError` 方法将会被调用。
315 |
316 | ```js
317 | loadUser()
318 | .then(function(user) {
319 | if (!user.activated) {
320 | throw new Error('user has not activated their account');
321 | }
322 | return showUserGallery(user);
323 | })
324 | .catch(function(err) {
325 | showError(err.message);
326 | });
327 | ```
328 |
329 | Promise 标准的这部分经常被视为它的一个陷阱。
330 | 它把所有错误处理的语义混淆了。语法错误,编码者错误(比如非法参数),和连接错误被糅合到相同的逻辑里去了。
331 |
332 | 这会给浏览器端开发带来麻烦:你可能无法调试,无法追查(你想要看到的)调用栈。
333 |
334 | 
335 |
336 | 对大多数开发者来说,就这个理由就足以让他们抛弃 promise 回归 error-first 回调风格和类似 [async](#async) 这样的工具.
337 |
338 | ## 常见模式
339 |
340 | ### memoization
341 |
342 | 我们在异步任务完成后使用 `.then()`。比如,我们可以缓存第一次请求的结果,resolve 同一个 `Image` 对象,而不是每次都请求同样的 `'not-found.png'` 图片。
343 |
344 | ```js
345 | var notFound;
346 |
347 | function getNotFoundImage() {
348 | if (notFound) {
349 | return notFound;
350 | }
351 | notFound = loadImageAsync('not-found.png');
352 | return notFound;
353 | }
354 | ```
355 |
356 | 这在服务端可能更有用,因为浏览器已经有缓存机制。
357 |
358 | ### `Promise.resolve` / `Promise.reject`
359 |
360 | `Promise` 本身也提供 `resolve` 和 `reject` 方法。调用它们时,将会返回一个新的 promise,这个新的 promise 已经是 resolved 或 rejected 状态。
361 |
362 | 举个例子:
363 |
364 | ```js
365 | var thumbnail = Promise.resolve(defaultThumbnail);
366 |
367 | // 查询数据库
368 | if (userLoggedIn) {
369 | thumbnail = loadUserThumbnail();
370 | }
371 |
372 | // 当 DOM 是 ready 状态时,添加图片到 DOM
373 | thumbnail.then(function(image) {
374 | document.body.appendChild(image);
375 | });
376 | ```
377 |
378 | 这里的 `loadUserThumbnail` 返回一个 `Promise`,并可以从中取到一个图片。有了 `Promise.resolve`,即使我们不去进行查询数据库的操作,也同样可以获得一个 `Promise`,并且不需要改动后面的代码。
379 |
380 | ### handling user errors
381 |
382 | 返回 promise 的函数应该*总是*返回 promise,这样使用它的时候就不需要用 `try/catch` 包裹这些函数。
383 |
384 | 在出现错误是,你应该使用 reject 返回错误,而不是抛出一个错误。[Promise.reject()](#promiseresolve--promisereject) 在这种场景下非常好用。
385 |
386 | 举个例子,这里用到了早先定义的 [`loadImageAsync`](#new-promise):
387 |
388 | ```js
389 | function loadImageAsync(url) {
390 | if (typeof url !== 'string') {
391 | return Promise.reject(new TypeError('must specify a string'));
392 | }
393 |
394 | return new Promise(function (resolve, reject) {
395 | /* async code */
396 | });
397 | }
398 | ```
399 |
400 | 或者可以在 promise 函数内部使用 `throw`:
401 |
402 | ```js
403 | function loadImageAsync(url) {
404 | return new Promise(function (resolve, reject) {
405 | if (typeof url !== 'string') {
406 | throw new TypeError('must specify a string');
407 | }
408 |
409 | /* async code */
410 | });
411 | }
412 | ```
413 |
414 | 点击[这里](https://www.w3.org/2001/tag/doc/promises-guide#always-return-promises) 可以了解更多细节。
415 |
416 | ### ES2015 中的 `Promise`
417 |
418 | 尽管本文使用了 [bluebird](https://github.com/petkaantonov/bluebird),但上述也同样适用于标准的 Promise 实现。比如,[Babel](https://babeljs.io/docs/learn-es2015/#promises) 中的 Promise。
419 |
420 | 一些其它的实现:
421 |
422 | - [pinkie-promise](https://github.com/floatdrop/pinkie-promise)
423 | - [es6-promise](https://www.npmjs.com/package/es6-promise)
424 |
425 | 举个例子,在 Node/browserify 中:
426 |
427 | ```js
428 | // 使用原生 promise ,如果不存在否则使用 polyfill
429 | var Promise = global.Promise || require('es6-promise').Promise;
430 | ```
431 |
432 | ## 陷阱
433 |
434 | 除了 [`throw` 和隐式 catch](#`throw` 和隐式 catch) 这个陷阱,还有一些问题值得注意。
435 |
436 | ### 小模块中的 promise
437 |
438 | 有一个不适合使用 promise 的场景,就是它不适合加进一些独立、小巧的 [npm](https://www.npmjs.com/) 模块。
439 |
440 | - 当打包大小有限制时,依赖 `bluebird` 或 `es6-promise` 占用一些空间。
441 | - 使用 `Promise` (ES2015) 构造器也会有问题,因为它引入了对那些 polyfill 的依赖。
442 | - 跨模块地混合使用不同的 promise 实现会导致一些微妙的 bug,调试时让人受尽折磨。
443 |
444 | 除非原生 Promise 普及,在小模块中,建议使用 Node 风格的回调和独立的 [async](#async) 模块来控制异步任务。
445 |
446 | 你可以使用任何一张你喜欢的 Promise 实现去 "promise 化"你的 API。比如,在 Bluebird 下使用 [xhr](https://www.npmjs.com/package/xhr) 模块:
447 |
448 | ```js
449 | var Promise = require('bluebird')
450 | var xhrAsync = Promise.promisify(require('xhr'))
451 | ```
452 |
453 | ### 复杂性
454 |
455 | Promises 引入了很多的复杂性和额外的智力开销。在实际项目中,开发者经常要面对基于 promise 的代码,但不完全明白 promise 内里的机制。
456 |
457 | 请看 Nolan Lawson 的 ["We Have a Problem With Promises"](http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html) 一文,里面给出了很多种 promise 的错误用法。
458 |
459 | ### lock-in
460 |
461 | 另一个令人沮丧的事实是,一旦你用了 promise,你就得在整个项目中使用它,以确保它能完美运行。
462 | 在实践中,你会发现,想要获得 promise 的诸多益处,需要先重构并 “promise 化” 很多代码。
463 | 这也意味着,写新的代码时必须以 promise 的方式思考——你会被这种思维方式折磨的,如果不熟练的话。
464 |
465 | ## 延伸阅读
466 |
467 | - [JavaScript Promises: There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/)
468 | - [We Have a Problem With Promises](http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html)
469 | - [You're Missing the Point of Promises](https://blog.domenic.me/youre-missing-the-point-of-promises/)
470 |
471 | ## 协议
472 |
473 | MIT,请看 [LICENSE.md](http://github.com/mattdesl/promise-cookbook/blob/master/LICENSE.md) 。
474 |
--------------------------------------------------------------------------------