|
1 | 1 | Promise 进阶
|
2 |
| -======== |
| 2 | +======== |
| 3 | + |
| 4 | +接下来的时间,我会继续把 Promise 的知识补完。 |
| 5 | + |
| 6 | +## `Promise.reject(reason)` |
| 7 | + |
| 8 | +这个方法比较简单,就返回一个状态为 `rejected` 的 Promise 实例。 |
| 9 | + |
| 10 | +它接受一个参数,为了方便后续操作,最好创建一个 Error 实例。 |
| 11 | + |
| 12 | +## `Promise.all()` |
| 13 | + |
| 14 | +`Promise.all([p1, p2, p3, ....])` 用于将多个 Promise 实例,包装成一个新的 Promise 实例。 |
| 15 | + |
| 16 | +它接受一个数组作为参数,数组里可以是 Promise 对象,也可以是别的值,这些值都会交给 `Promise.resolve()` 处理。当所有子 Promise 都完成,该 Promise 完成,返回值是包含全部返回值的数组。有任何一个失败,该 Promise 失败,`.catch()` 得到的是第一个失败的子 Promise 的错误。 |
| 17 | + |
| 18 | +```javascript |
| 19 | +Promise.all([1, 2, 3]) |
| 20 | + .then( all => { |
| 21 | + console.log('1: ', all); |
| 22 | + return Promise.all([ function () { |
| 23 | + console.log('ooxx'); |
| 24 | + }, 'xxoo', false]); |
| 25 | + }) |
| 26 | + .then( all => { |
| 27 | + console.log('2: ', all); |
| 28 | + let p1 = new Promise( resolve => { |
| 29 | + setTimeout(() => { |
| 30 | + resolve('I\'m P1'); |
| 31 | + }, 1500); |
| 32 | + }); |
| 33 | + let p2 = new Promise( resolve => { |
| 34 | + setTimeout(() => { |
| 35 | + resolve('I\'m P2'); |
| 36 | + }, 1450) |
| 37 | + }); |
| 38 | + return Promise.all([p1, p2]); |
| 39 | + }) |
| 40 | + .then( all => { |
| 41 | + console.log('3: ', all); |
| 42 | + let p1 = new Promise( resolve => { |
| 43 | + setTimeout(() => { |
| 44 | + resolve('I\'m P1'); |
| 45 | + }, 1500); |
| 46 | + }); |
| 47 | + let p2 = new Promise( (resolve, reject) => { |
| 48 | + setTimeout(() => { |
| 49 | + reject('I\'m P2'); |
| 50 | + }, 1000); |
| 51 | + }); |
| 52 | + let p3 = new Promise( (resolve , reject) => { |
| 53 | + setTimeout(() => { |
| 54 | + reject('I\'m P3'); |
| 55 | + }, 3000); |
| 56 | + }); |
| 57 | + return Promise.all([p1, p2, p3]); |
| 58 | + }) |
| 59 | + .then( all => { |
| 60 | + console.log('all', all); |
| 61 | + }) |
| 62 | + .catch( err => { |
| 63 | + console.log('Catch: ', err); |
| 64 | + }); |
| 65 | + |
| 66 | +// 输出: |
| 67 | +// 1: [ 1, 2, 3 ] |
| 68 | +// 2: [ [Function], 'xxoo', false ] |
| 69 | +// 3: [ 'I\'m P1', 'I\'m P2' ] |
| 70 | +// Catch: I'm P2 |
| 71 | +``` |
| 72 | + |
| 73 | +这里很容易懂,就不一一解释了。 |
| 74 | + |
| 75 | +### 常见用法 |
| 76 | + |
| 77 | +`Promise.all()` 最常见就是和 `.map()` 连用。 |
| 78 | + |
| 79 | +我们改造一下前面的例子。 |
| 80 | + |
| 81 | +```javascript |
| 82 | +// FileSystem.js |
| 83 | +const fs = require('fs'); |
| 84 | + |
| 85 | +module.exports = { |
| 86 | + readDir: function (path, options) { |
| 87 | + return new Promise( resolve => { |
| 88 | + fs.readdir(path, options, (err, files) => { |
| 89 | + if (err) { |
| 90 | + throw err; |
| 91 | + } |
| 92 | + resolve(files); |
| 93 | + }); |
| 94 | + }); |
| 95 | + }, |
| 96 | + stat: function (path, options) { |
| 97 | + return new Promise( resolve => { |
| 98 | + fs.stat(path, options, (err, stat) => { |
| 99 | + if (err) { |
| 100 | + throw err; |
| 101 | + } |
| 102 | + resolve(stat); |
| 103 | + }); |
| 104 | + }); |
| 105 | + } |
| 106 | +}; |
| 107 | + |
| 108 | +// main.js |
| 109 | +const fs = require('./FileSystem'); |
| 110 | + |
| 111 | +function findLargest(dir) { |
| 112 | + return fs.readDir(dir, 'utf-8') |
| 113 | + .then( files => { |
| 114 | + return Promise.all( files.map( file => fs.stat(file) )); |
| 115 | + }) |
| 116 | + .then( stats => { |
| 117 | + let biggest = stats.reduce( (memo, stat) => { |
| 118 | + if (stat.isDirectory()) { |
| 119 | + return memo; |
| 120 | + } |
| 121 | + if (memo.size < stat.size) { |
| 122 | + return stat; |
| 123 | + } |
| 124 | + return memo; |
| 125 | + }); |
| 126 | + return biggest.file; |
| 127 | + }) |
| 128 | + .catch(console.log.bind(console)); |
| 129 | +} |
| 130 | + |
| 131 | +findLargest('some/path/') |
| 132 | + .then( file => { |
| 133 | + console.log(file); |
| 134 | + }) |
| 135 | + .catch( err => { |
| 136 | + console.log(err); |
| 137 | + }); |
| 138 | +``` |
| 139 | + |
| 140 | +在这个例子当中,我使用 Promise 将 `fs.stat` 和 `fs.readdir` 进行了封装,让其返回 Promise 对象。然后使用 `Promise.all()` + `array.map()` 方法,就可以对其进行遍历,还可以避免使用外层作用域的变量。 |
| 141 | + |
| 142 | +## `Promise.race()` |
| 143 | + |
| 144 | +`Promise.race()` 的功能和用法与 `Promise.all()` 十分类似,也接受一个数组作为参数,然后把数组里的值都用 `Promise.resolve()` 处理成 Promise 对象,然后再返回一个新的 Promise 实例。只不过这些子 Promise 有任意一个完成,`Promise.race()` 返回的 Promise 实例就算完成,并且返回完成的子实例的返回值。 |
| 145 | + |
| 146 | +它最常见的用法,是作为超时检查。我们可以把异步操作和定时器放在一个 `Promise.race()` 里,如果定时器触发的时候异步操作还没返回,就可以认为超时了。 |
| 147 | + |
| 148 | +```javascript |
| 149 | +let p1 = new Promise(resolve => { |
| 150 | + // 这是一个长时间的调用,我们假装它就是正常要跑的异步操作 |
| 151 | + setTimeout(() => { |
| 152 | + resolve('I\'m P1'); |
| 153 | + }, 10000); |
| 154 | +}); |
| 155 | +let p2 = new Promise(resolve => { |
| 156 | + // 这是个稍短的调用,假装是一个定时器 |
| 157 | + setTimeout(() => { |
| 158 | + resolve(false); |
| 159 | + }, 2000) |
| 160 | +}); |
| 161 | +Promise.race([p1, p2]) |
| 162 | + .then(value => { |
| 163 | + if (value) { |
| 164 | + console.log(value); |
| 165 | + } else { |
| 166 | + console.log('Timeout, Yellow flower is cold'); |
| 167 | + } |
| 168 | + }); |
| 169 | + |
| 170 | +// 输出: |
| 171 | +// Timeout, Yellow flower is cold |
| 172 | +``` |
| 173 | + |
| 174 | +## Promise 嵌套 |
| 175 | + |
| 176 | +这种情况在初用 Promise 的同学的代码中很常见,大概是这么个意思: |
| 177 | + |
| 178 | +```javascript |
| 179 | +new Promise( resolve => { |
| 180 | + console.log('Step 1'); |
| 181 | + setTimeout(() => { |
| 182 | + resolve(100); |
| 183 | + }, 1000); |
| 184 | +}) |
| 185 | + .then( value => { |
| 186 | + return new Promise(resolve => { |
| 187 | + console.log('Step 1-1'); |
| 188 | + setTimeout(() => { |
| 189 | + resolve(110); |
| 190 | + }, 1000); |
| 191 | + }) |
| 192 | + .then( value => { |
| 193 | + console.log('Step 1-2'); |
| 194 | + return value; |
| 195 | + }) |
| 196 | + .then( value => { |
| 197 | + console.log('Step 1-3'); |
| 198 | + return value; |
| 199 | + }); |
| 200 | + }) |
| 201 | + .then(value => { |
| 202 | + console.log(value); |
| 203 | + console.log('Step 2'); |
| 204 | + }); |
| 205 | +``` |
| 206 | + |
| 207 | +因为 `.then()` 返回的也是 Promise 实例,所以外层的 Promise 会等里面的 `.then()` 执行完再继续执行,所以这里的执行顺序稳定为“1 > 1-1 > 1-2 > 1-3 > 2”。但是从阅读体验和维护效率的角度来看,最好把它展开: |
| 208 | + |
| 209 | +```javascript |
| 210 | +new Promise( resolve => { |
| 211 | + console.log('Step 1'); |
| 212 | + setTimeout(() => { |
| 213 | + resolve(100); |
| 214 | + }, 1000); |
| 215 | +}) |
| 216 | + .then( value => { |
| 217 | + return new Promise(resolve => { |
| 218 | + console.log('Step 1-1'); |
| 219 | + setTimeout(() => { |
| 220 | + resolve(110); |
| 221 | + }, 1000); |
| 222 | + }); |
| 223 | + }) |
| 224 | + .then( value => { |
| 225 | + console.log('Step 1-2'); |
| 226 | + return value; |
| 227 | + }) |
| 228 | + .then( value => { |
| 229 | + console.log('Step 1-3'); |
| 230 | + return value; |
| 231 | + }) |
| 232 | + .then(value => { |
| 233 | + console.log(value); |
| 234 | + console.log('Step 2'); |
| 235 | + }); |
| 236 | +``` |
| 237 | + |
| 238 | +## 队列 |
| 239 | + |
| 240 | +有时候我们不希望所有动作一起发生,而是按照一定顺序,逐个进行。这样的形式,就是队列。在我看来,队列是 Promise 的核心用法。即使在异步函数实装的今天,队列也有其独特的价值。 |
| 241 | + |
| 242 | +用 Promise 实现队列的方式很多,这里兹举两例: |
| 243 | + |
| 244 | +```javascript |
| 245 | +// 使用 Array.prototype.forEach |
| 246 | +function queue(things) { |
| 247 | + let promise = Promise.resolve(); |
| 248 | + things.forEach( thing => { |
| 249 | + // 这里很重要,如果不把 `.then()` 返回的新实例赋给 `promise` 的话,就不是队列,而是批量执行 |
| 250 | + promise = promise.then( () => { |
| 251 | + return new Promise( resolve => { |
| 252 | + doThing(thing, () => { |
| 253 | + resolve(); |
| 254 | + }); |
| 255 | + }); |
| 256 | + }); |
| 257 | + }); |
| 258 | + return promise; |
| 259 | +} |
| 260 | + |
| 261 | +queue(['lots', 'of', 'things', ....]); |
| 262 | + |
| 263 | +// 使用 Array.prototype.reduce |
| 264 | +function queue(things) { |
| 265 | + return things.reduce( (promise, thing) => { |
| 266 | + return promise.then( () => { |
| 267 | + return new Promise( resolve => { |
| 268 | + doThing(thing, () => { |
| 269 | + resolve(); |
| 270 | + }); |
| 271 | + }); |
| 272 | + }); |
| 273 | + }, Promise.resolve()); |
| 274 | +} |
| 275 | + |
| 276 | +queue(['lots', 'of', 'things', ....]); |
| 277 | +``` |
| 278 | + |
| 279 | +这个例子如此直接我就不再详细解释了。下面我们看一个相对复杂的例子, |
| 280 | + |
| 281 | +> 假设需求:开发一个爬虫,抓取某网站。 |
| 282 | +
|
| 283 | +```javascript |
| 284 | +function fetchAll(urls) { |
| 285 | + return urls.reduce((promise, url) => { |
| 286 | + return promise.then( () => { |
| 287 | + return fetch(url); |
| 288 | + }); |
| 289 | + }, Promise.resolve()); |
| 290 | +} |
| 291 | +function fetch(url) { |
| 292 | + return spider.fetch(url) |
| 293 | + .then( content => { |
| 294 | + return saveOrOther(content); |
| 295 | + }) |
| 296 | + .then( content => { |
| 297 | + let links = spider.findLinks(content); |
| 298 | + return fetchAll(links); |
| 299 | + }); |
| 300 | +} |
| 301 | +let url = ['http://blog.meathill.com/']; |
| 302 | +fetchAll(url); |
| 303 | +``` |
| 304 | + |
| 305 | +这段代码,我假设有一个蜘蛛工具(spider)包含基本的抓取和分析功能,然后循环使用 `fetch` 和 `fetchAll` 方法,不断分析抓取的页面,然后把页面当中所有的链接都加入到抓取队列当中。 |
| 306 | + |
| 307 | +### Generator |
| 308 | + |
| 309 | +如果你了解 Generator,你应该知道 Generator 可以在执行时中断,并等待被唤醒。如果能把它们连到一起使用应该不错。 |
| 310 | + |
| 311 | +```javascript |
| 312 | +let generator = function* (urls) { |
| 313 | + let loaded = []; |
| 314 | + while (urls.length > 0) { |
| 315 | + let url = urls.unshift(); |
| 316 | + yield spider.fetch(url) |
| 317 | + .then( content => { |
| 318 | + loaded.push(url); |
| 319 | + return saveOrOther(content); |
| 320 | + }) |
| 321 | + .then( content => { |
| 322 | + let links = spider.findLinks(content); |
| 323 | + links = _.without(links, loaded); |
| 324 | + urls = urls.concat(links); |
| 325 | + }); |
| 326 | + } |
| 327 | + return 'over'; |
| 328 | +}; |
| 329 | + |
| 330 | +function fetch(urls) { |
| 331 | + let iterator = generator(); |
| 332 | + |
| 333 | + function next() { |
| 334 | + let result = iterator.next(); |
| 335 | + if (result.done) { |
| 336 | + return result.value; |
| 337 | + } |
| 338 | + let promise = iterator.next().value; |
| 339 | + promise.then(next); |
| 340 | + } |
| 341 | + |
| 342 | + next(); |
| 343 | +} |
| 344 | + |
| 345 | +let urls = ['http://blog.meathill.com']; |
| 346 | +fetch(urls); |
| 347 | +``` |
| 348 | + |
| 349 | +Generator 可以把所有待抓取的 URL 都放到一个数组里,然后慢慢加载。从整体来看,暴露给外界的 `fetch` 函数其实变简单了很多。但是实现 Generator 本身有点费工夫,其中的利弊大家自己权衡吧。 |
0 commit comments