|
| 1 | +# Promise 对象 |
| 2 | + |
| 3 | +## 概述 |
| 4 | + |
| 5 | +Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。 |
| 6 | + |
| 7 | +注意,本章只是 Promise 对象的简单介绍。为了避免与后续教程的重复,更完整的介绍请看[《ES6 标准入门》](http://es6.ruanyifeng.com/)的[《Promise 对象》](http://es6.ruanyifeng.com/#docs/promise)一章。 |
| 8 | + |
| 9 | +首先,Promise 是一个对象,也是一个构造函数。 |
| 10 | + |
| 11 | +```javascript |
| 12 | +function f1(resolve, reject) { |
| 13 | + // 异步代码... |
| 14 | +} |
| 15 | + |
| 16 | +var p1 = new Promise(f1); |
| 17 | +``` |
| 18 | + |
| 19 | +上面代码中,`Promise`构造函数接受一个回调函数`f1`作为参数,`f1`里面是异步操作的代码。然后,返回的`p1`就是一个 Promise 实例。 |
| 20 | + |
| 21 | +Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个`then`方法,用来指定下一步的回调函数。 |
| 22 | + |
| 23 | +```javascript |
| 24 | +var p1 = new Promise(f1); |
| 25 | +p1.then(f2); |
| 26 | +``` |
| 27 | + |
| 28 | +上面代码中,`f1`的异步操作执行完成,就会执行`f2`。 |
| 29 | + |
| 30 | +传统的写法可能需要把`f2`作为回调函数传入`f1`,比如写成`f1(f2)`,异步操作完成后,在`f1`内部调用`f2`。Promise 使得`f1`和`f2`变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。 |
| 31 | + |
| 32 | +```javascript |
| 33 | +// 传统写法 |
| 34 | +step1(function (value1) { |
| 35 | + step2(value1, function(value2) { |
| 36 | + step3(value2, function(value3) { |
| 37 | + step4(value3, function(value4) { |
| 38 | + // ... |
| 39 | + }); |
| 40 | + }); |
| 41 | + }); |
| 42 | +}); |
| 43 | + |
| 44 | +// Promise 的写法 |
| 45 | +(new Promise(step1)) |
| 46 | + .then(step2) |
| 47 | + .then(step3) |
| 48 | + .then(step4); |
| 49 | +``` |
| 50 | + |
| 51 | +从上面代码可以看到,采用 Promises 以后,程序流程变得非常清楚,十分易读。注意,为了便于理解,上面代码的`Promise`实例的生成格式,做了简化,真正的语法请参照下文。 |
| 52 | + |
| 53 | +总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promise 就是解决这个问题,使得异步流程可以写成同步流程。 |
| 54 | + |
| 55 | +Promise 原本只是社区提出的一个构想,一些函数库率先实现了这个功能。ECMAScript 6 将其写入语言标准,目前 JavaScript 原生支持 Promise 对象。 |
| 56 | + |
| 57 | +## Promise 对象的状态 |
| 58 | + |
| 59 | +Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。 |
| 60 | + |
| 61 | +- 异步操作未完成(pending) |
| 62 | +- 异步操作成功(fulfilled) |
| 63 | +- 异步操作失败(rejected) |
| 64 | + |
| 65 | +上面三种状态里面,`fulfilled`和`rejected`合在一起称为`resolved`(已定型)。 |
| 66 | + |
| 67 | +这三种的状态的变化途径只有两种。 |
| 68 | + |
| 69 | +- 从“未完成”到“成功” |
| 70 | +- 从“未完成”到“失败” |
| 71 | + |
| 72 | +一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。 |
| 73 | + |
| 74 | +因此,Promise 的最终结果只有两种。 |
| 75 | + |
| 76 | +- 异步操作成功,Promise 实例传回一个值(value),状态变为`fulfilled`。 |
| 77 | +- 异步操作失败,Promise 实例抛出一个错误(error),状态变为`rejected`。 |
| 78 | + |
| 79 | +## Promise 构造函数 |
| 80 | + |
| 81 | +JavaScript 提供原生的`Promise`构造函数,用来生成 Promise 实例。 |
| 82 | + |
| 83 | +```javascript |
| 84 | +var promise = new Promise(function (resolve, reject) { |
| 85 | + // ... |
| 86 | + |
| 87 | + if (/* 异步操作成功 */){ |
| 88 | + resolve(value); |
| 89 | + } else { /* 异步操作失败 */ |
| 90 | + reject(new Error()); |
| 91 | + } |
| 92 | +}); |
| 93 | +``` |
| 94 | + |
| 95 | +上面代码中,`Promise`构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve`和`reject`。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。 |
| 96 | + |
| 97 | +`resolve`函数的作用是,将`Promise`实例的状态从“未完成”变为“成功”(即从`pending`变为`fulfilled`),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。`reject`函数的作用是,将`Promise`实例的状态从“未完成”变为“失败”(即从`pending`变为`rejected`),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 |
| 98 | + |
| 99 | +下面是一个例子。 |
| 100 | + |
| 101 | +```javascript |
| 102 | +function timeout(ms) { |
| 103 | + return new Promise((resolve, reject) => { |
| 104 | + setTimeout(resolve, ms, 'done'); |
| 105 | + }); |
| 106 | +} |
| 107 | + |
| 108 | +timeout(100) |
| 109 | +``` |
| 110 | + |
| 111 | +上面代码中,`timeout(100)`返回一个 Promise 实例。100毫秒以后,该实例的状态会变为`fulfilled`。 |
| 112 | + |
| 113 | +## Promise.prototype.then() |
| 114 | + |
| 115 | +Promise 实例的`then`方法,用来添加回调函数。 |
| 116 | + |
| 117 | +`then`方法可以接受两个回调函数,第一个是异步操作成功时(变为`fulfilled`状态)时的回调函数,第二个是异步操作失败(变为`rejected`)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。 |
| 118 | + |
| 119 | +```javascript |
| 120 | +var p1 = new Promise(function (resolve, reject) { |
| 121 | + resolve('成功'); |
| 122 | +}); |
| 123 | +p1.then(console.log, console.error); |
| 124 | +// "成功" |
| 125 | + |
| 126 | +var p2 = new Promise(function (resolve, reject) { |
| 127 | + reject(new Error('失败')); |
| 128 | +}); |
| 129 | +p2.then(console.log, console.error); |
| 130 | +// Error: 失败 |
| 131 | +``` |
| 132 | + |
| 133 | +上面代码中,`p1`和`p2`都是Promise 实例,它们的`then`方法绑定两个回调函数:成功时的回调函数`console.log`,失败时的回调函数`console.error`(可以省略)。`p1`的状态变为成功,`p2`的状态变为失败,对应的回调函数会收到异步操作传回的值,然后在控制台输出。 |
| 134 | + |
| 135 | +`then`方法可以链式使用。 |
| 136 | + |
| 137 | +```javascript |
| 138 | +p1 |
| 139 | + .then(step1) |
| 140 | + .then(step2) |
| 141 | + .then(step3) |
| 142 | + .then( |
| 143 | + console.log, |
| 144 | + console.error |
| 145 | + ); |
| 146 | +``` |
| 147 | + |
| 148 | +上面代码中,`p1`后面有四个`then`,意味依次有四个回调函数。只要前一步的状态变为`fulfilled`,就会依次执行紧跟在后面的回调函数。 |
| 149 | + |
| 150 | +最后一个`then`方法,回调函数是`console.log`和`console.error`,用法上有一点重要的区别。`console.log`只显示`step3`的返回值,而`console.error`可以显示`p1`、`step1`、`step2`、`step3`之中任意一个发生的错误。举例来说,如果`step1`的状态变为`rejected`,那么`step2`和`step3`都不会执行了(因为它们是`resolved`的回调函数)。Promise 开始寻找,接下来第一个为`rejected`的回调函数,在上面代码中是`console.error`。这就是说,Promise 对象的报错具有传递性。 |
| 151 | + |
| 152 | +## then() 用法辨析 |
| 153 | + |
| 154 | +Promise 的用法,简单说就是一句话:使用`then`方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里? |
| 155 | + |
| 156 | +```javascript |
| 157 | +// 写法一 |
| 158 | +f1().then(function () { |
| 159 | + return f2(); |
| 160 | +}); |
| 161 | + |
| 162 | +// 写法二 |
| 163 | +f1().then(function () { |
| 164 | + f2(); |
| 165 | +}); |
| 166 | + |
| 167 | +// 写法三 |
| 168 | +f1().then(f2()); |
| 169 | + |
| 170 | +// 写法四 |
| 171 | +f1().then(f2); |
| 172 | +``` |
| 173 | + |
| 174 | +为了便于讲解,下面这四种写法都再用`then`方法接一个回调函数`f3`。写法一的`f3`回调函数的参数,是`f2`函数的运行结果。 |
| 175 | + |
| 176 | +```javascript |
| 177 | +f1().then(function () { |
| 178 | + return f2(); |
| 179 | +}).then(f3); |
| 180 | +``` |
| 181 | + |
| 182 | +写法二的`f3`回调函数的参数是`undefined`。 |
| 183 | + |
| 184 | +```javascript |
| 185 | +f1().then(function () { |
| 186 | + f2(); |
| 187 | + return; |
| 188 | +}).then(f3); |
| 189 | +``` |
| 190 | + |
| 191 | +写法三的`f3`回调函数的参数,是`f2`函数返回的函数的运行结果。 |
| 192 | + |
| 193 | +```javascript |
| 194 | +f1().then(f2()) |
| 195 | + .then(f3); |
| 196 | +``` |
| 197 | + |
| 198 | +写法四与写法一只有一个差别,那就是`f2`会接收到`f1()`返回的结果。 |
| 199 | + |
| 200 | +```javascript |
| 201 | +f1().then(f2) |
| 202 | + .then(f3); |
| 203 | +``` |
| 204 | + |
| 205 | +## Promise 的实例 |
| 206 | + |
| 207 | +### 加载图片 |
| 208 | + |
| 209 | +我们可以把图片的加载写成一个`Promise`对象。 |
| 210 | + |
| 211 | +```javascript |
| 212 | +var preloadImage = function (path) { |
| 213 | + return new Promise(function (resolve, reject) { |
| 214 | + var image = new Image(); |
| 215 | + image.onload = resolve; |
| 216 | + image.onerror = reject; |
| 217 | + image.src = path; |
| 218 | + }); |
| 219 | +}; |
| 220 | +``` |
| 221 | + |
| 222 | +### Ajax 操作 |
| 223 | + |
| 224 | +Ajax 操作是典型的异步操作,传统上往往写成下面这样。 |
| 225 | + |
| 226 | +```javascript |
| 227 | +function search(term, onload, onerror) { |
| 228 | + var xhr, results, url; |
| 229 | + url = 'http://example.com/search?q=' + term; |
| 230 | + |
| 231 | + xhr = new XMLHttpRequest(); |
| 232 | + xhr.open('GET', url, true); |
| 233 | + |
| 234 | + xhr.onload = function (e) { |
| 235 | + if (this.status === 200) { |
| 236 | + results = JSON.parse(this.responseText); |
| 237 | + onload(results); |
| 238 | + } |
| 239 | + }; |
| 240 | + xhr.onerror = function (e) { |
| 241 | + onerror(e); |
| 242 | + }; |
| 243 | + |
| 244 | + xhr.send(); |
| 245 | +} |
| 246 | + |
| 247 | +search('Hello World', console.log, console.error); |
| 248 | +``` |
| 249 | + |
| 250 | +如果使用 Promise 对象,就可以写成下面这样。 |
| 251 | + |
| 252 | +```javascript |
| 253 | +function search(term) { |
| 254 | + var url = 'http://example.com/search?q=' + term; |
| 255 | + var xhr = new XMLHttpRequest(); |
| 256 | + var result; |
| 257 | + |
| 258 | + var p = new Promise(function (resolve, reject) { |
| 259 | + xhr.open('GET', url, true); |
| 260 | + xhr.onload = function (e) { |
| 261 | + if (this.status === 200) { |
| 262 | + result = JSON.parse(this.responseText); |
| 263 | + resolve(result); |
| 264 | + } |
| 265 | + }; |
| 266 | + xhr.onerror = function (e) { |
| 267 | + reject(e); |
| 268 | + }; |
| 269 | + xhr.send(); |
| 270 | + }); |
| 271 | + |
| 272 | + return p; |
| 273 | +} |
| 274 | + |
| 275 | +search('Hello World').then(console.log, console.error); |
| 276 | +``` |
| 277 | + |
| 278 | +加载图片的例子,也可以用 Ajax 操作完成。 |
| 279 | + |
| 280 | +```javascript |
| 281 | +function imgLoad(url) { |
| 282 | + return new Promise(function (resolve, reject) { |
| 283 | + var request = new XMLHttpRequest(); |
| 284 | + request.open('GET', url); |
| 285 | + request.responseType = 'blob'; |
| 286 | + request.onload = function () { |
| 287 | + if (request.status === 200) { |
| 288 | + resolve(request.response); |
| 289 | + } else { |
| 290 | + reject(new Error('图片加载失败:' + request.statusText)); |
| 291 | + } |
| 292 | + }; |
| 293 | + request.onerror = function () { |
| 294 | + reject(new Error('发生网络错误')); |
| 295 | + }; |
| 296 | + request.send(); |
| 297 | + }); |
| 298 | +} |
| 299 | +``` |
| 300 | + |
| 301 | +## 小结 |
| 302 | + |
| 303 | +Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。 |
| 304 | + |
| 305 | +而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。 |
| 306 | + |
| 307 | +Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆`then`,必须自己在`then`的回调函数里面理清逻辑。 |
| 308 | + |
| 309 | +## 微任务 |
| 310 | + |
| 311 | +Promise 的回调函数属于异步任务,会在同步任务之后执行。 |
| 312 | + |
| 313 | +```javascript |
| 314 | +new Promise(function (resolve, reject) { |
| 315 | + resolve(1); |
| 316 | +}).then(console.log); |
| 317 | + |
| 318 | +console.log(2); |
| 319 | +// 2 |
| 320 | +// 1 |
| 321 | +``` |
| 322 | + |
| 323 | +上面代码会先输出2,再输出1。因为`console.log(2)`是同步任务,而`then`的回调函数属于异步任务,一定晚于同步任务执行。 |
| 324 | + |
| 325 | +但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。 |
| 326 | + |
| 327 | +```javascript |
| 328 | +setTimeout(function() { |
| 329 | + console.log(1); |
| 330 | +}, 0); |
| 331 | + |
| 332 | +new Promise(function (resolve, reject) { |
| 333 | + resolve(2); |
| 334 | +}).then(console.log); |
| 335 | + |
| 336 | +console.log(3); |
| 337 | +// 3 |
| 338 | +// 2 |
| 339 | +// 1 |
| 340 | +``` |
| 341 | + |
| 342 | +上面代码的输出结果是`321`。这说明`then`的回调函数的执行时间,早于`setTimeout(fn, 0)`。因为`then`是本轮事件循环执行,`setTimeout(fn, 0)`在下一轮事件循环开始时执行。 |
| 343 | + |
| 344 | +## 参考链接 |
| 345 | + |
| 346 | +- Sebastian Porto, [Asynchronous JS: Callbacks, Listeners, Control Flow Libs and Promises](http://sporto.github.com/blog/2012/12/09/callbacks-listeners-promises/) |
| 347 | +- Rhys Brett-Bowen, [Promises/A+ - understanding the spec through implementation](http://modernjavascript.blogspot.com/2013/08/promisesa-understanding-by-doing.html) |
| 348 | +- Matt Podwysocki, Amanda Silver, [Asynchronous Programming in JavaScript with “Promises”](http://blogs.msdn.com/b/ie/archive/2011/09/11/asynchronous-programming-in-javascript-with-promises.aspx) |
| 349 | +- Marc Harter, [Promise A+ Implementation](https://gist.github.com//wavded/5692344) |
| 350 | +- Bryan Klimt, [What’s so great about JavaScript Promises?](http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/) |
| 351 | +- Jake Archibald, [JavaScript Promises There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/) |
| 352 | +- Mikito Takada, [7. Control flow, Mixu's Node book](http://book.mixu.net/node/ch7.html) |
0 commit comments