├── .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 | ![debugging](http://i.imgur.com/Y6RH8ke.png) 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 | ![debugging](http://i.imgur.com/Y6RH8ke.png) 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 | --------------------------------------------------------------------------------