Promise 进阶

接下来的时间,我会继续把 Promise 的知识补完。

Promise.reject(reason)

这个方法比较简单,就返回一个状态为 rejected 的 Promise 实例。

它接受一个参数 reason,作为状态说明,交由后面的 .catch() 捕获。为了与其它异常处理共用一个.catch(),我们可以用 Error 实例作为 reason

另外,Promise.reject() 也不认 thenable

let error = new Error('something wrong');
Promise.reject(error)
  .then( value => {
    console.log('it\'s ok');
    console.log(value);
  })
  .catch( err => {
    console.log('no, it\'s not ok');
    console.log(err);

    return Promise.reject({
      then() {
        console.log('it will be ok');
      },
      catch() {
        console.log('not yet');
      }
    });
  });

Promise.all([p1, p2, p3, ....])

Promise.all([p1, p2, p3, ....]) 用于将多个 Promise 实例,包装成一个新的 Promise 实例。

它接受一个数组(其实是 iterable,不过我觉得暂时不要引入更多概念了……)作为参数,数组里可以是 Promise 对象,也可以是别的值,这些值都会交给 Promise.resolve() 处理。当所有子 Promise 都完成,该 Promise 完成,返回值是包含全部返回值的数组。有任何一个失败,该 Promise 失败,.catch() 得到的是第一个失败的子 Promise 的错误。

Promise.all([1, 2, 3])
  .then( all => {
    console.log('1: ', all);
    return Promise.all([ function () {
      console.log('ooxx');
    }, 'xxoo', false]);
  })
  .then( all => {
    console.log('2: ', all);
    let p1 = new Promise( resolve => {
      setTimeout(() => {
        resolve('I\'m P1');
      }, 1500);
    });
    let p2 = new Promise( resolve => {
      setTimeout(() => {
        resolve('I\'m P2');
      }, 1450)
    });
    return Promise.all([p1, p2]);
  })
  .then( all => {
    console.log('3: ', all);
    let p1 = new Promise( resolve => {
      setTimeout(() => {
        resolve('I\'m P1');
      }, 1500);
    });
    let p2 = new Promise( (resolve, reject) => {
      setTimeout(() => {
        reject('I\'m P2');
      }, 1000);
    });
    let p3 = new Promise( (resolve , reject) => {
      setTimeout(() => {
        reject('I\'m P3');
      }, 3000);
    });
    return Promise.all([p1, p2, p3]);
  })
  .then( all => {
    console.log('all', all);
  })
  .catch( err => {
    console.log('Catch: ', err);
  });

// 输出:
// 1:  [ 1, 2, 3 ]
// 2:  [ [Function], 'xxoo', false ]
// 3:  [ 'I\'m P1', 'I\'m P2' ]
// Catch:  I'm P2

这里很容易懂,就不一一解释了。

常见用法

Promise.all() 最常见就是和 .map() 连用。

我们改造一下前面的例子。

// FileSystem.js
const fs = require('fs');

module.exports = {
  readDir: function (path, options) {
    return new Promise( resolve => {
      fs.readdir(path, options, (err, files) => {
        if (err) {
          throw err;
        }
        resolve(files);
      });
    });
  },
  stat: function (path, options) {
    return new Promise( resolve => {
      fs.stat(path, options, (err, stat) => {
        if (err) {
          throw err;
        }
        resolve(stat);
      });
    });
  }
};

// main.js
const fs = require('./FileSystem');

function findLargest(dir) {
  return fs.readDir(dir, 'utf-8')
    .then( files => {
      return Promise.all( files.map( file => fs.stat(file) ));
    })
    .then( stats => {
      let biggest = stats.reduce( (memo, stat) => {
        if (stat.isDirectory()) {
          return memo;
        }
        if (memo.size < stat.size) {
          return stat;
        }
        return memo;
      });
      return biggest.file;
    })
    .catch(console.log.bind(console));
}

findLargest('some/path/')
  .then( file => {
    console.log(file);
  })
  .catch( err => {
    console.log(err);
  });

在这个例子当中,我使用 Promise 将 fs.statfs.readdir 进行了封装,让其返回 Promise 对象。然后使用 Promise.all() + Array.prototype.map() 方法,就可以进行遍历,还可以避免使用外层作用域的变量。

Promise.race([p1, p2, p3, ....])

Promise.race() 的功能和用法与 Promise.all() 十分类似,也接受一个数组作为参数,然后把数组里的值都用 Promise.resolve() 处理成 Promise 对象,然后再返回一个新的 Promise 实例。只不过这些子 Promise 有任意一个完成,Promise.race() 返回的 Promise 实例就算完成,并且返回完成的子实例的返回值。

它最常见的用法,是作超时检查。我们可以把异步操作和定时器放在一个 Promise.race() 里,如果定时器触发时异步操作还没返回,就可以认为超时了,然后就可以给用户一些提示。

let p1 = new Promise(resolve => {
  // 这是一个长时间的调用,我们假装它就是正常要跑的异步操作
  setTimeout(() => {
    resolve('I\'m P1');
  }, 10000);
});
let p2 = new Promise(resolve => {
  // 这是个稍短的调用,假装是一个定时器
  setTimeout(() => {
    resolve(false);
  }, 2000)
});
Promise.race([p1, p2])
  .then(value => {
    if (value) {
      console.log(value);
    } else {
      console.log('Timeout, Yellow flower is cold');
    }
  });

// 输出:
// Timeout, Yellow flower is cold

注意,这里 p1 也就是原本就要执行的异步操作并没有被中止,它只是没有在预期的时间内返回而已。所以一方面可以继续等待它的返回值,另一方面也要考虑服务器端是否需要做回滚处理。

Promise 嵌套

这种情况在初涉 Promise 的同学的代码中很常见,大概是这么个意思:

new Promise( resolve => {
  console.log('Step 1');
  setTimeout(() => {
    resolve(100);
  }, 1000);
})
  .then( value => {
    return new Promise(resolve => {
      console.log('Step 1-1');
      setTimeout(() => {
        resolve(110);
      }, 1000);
    })
      .then( value => {
        console.log('Step 1-2');
        return value;
      })
      .then( value => {
        console.log('Step 1-3');
        return value;
      });
  })
  .then(value => {
    console.log(value);
    console.log('Step 2');
  });

因为 .then() 返回的也是 Promise 实例,所以外层的 Promise 会等里面的 .then() 执行完再继续执行,所以这里的执行顺序稳定为从上之下,左右无关,“1 > 1-1 > 1-2 > 1-3 > 2”。但是从阅读体验和维护效率的角度来看,最好把它展开:

new Promise( resolve => {
  console.log('Step 1');
  setTimeout(() => {
    resolve(100);
  }, 1000);
})
  .then( value => {
    return new Promise(resolve => {
      console.log('Step 1-1');
      setTimeout(() => {
        resolve(110);
      }, 1000);
    });
  })
  .then( value => {
    console.log('Step 1-2');
    return value;
  })
  .then( value => {
    console.log('Step 1-3');
    return value;
  })
  .then(value => {
    console.log(value);
    console.log('Step 2');
  });

二者是完全等价的,后者更容易阅读。

队列

有时候我们不希望所有动作一起发生,而是按照一定顺序,逐个进行。这样的形式,就是队列。在我看来,队列是 Promise 的核心价值,即使是异步函数在大部分浏览器和 Node.js 里实装的今天,队列也仍有其独特的价值。

用 Promise 实现队列的方式很多,这里兹举两例:

// 使用 Array.prototype.forEach
function queue(things) {
  let promise = Promise.resolve();
  things.forEach( thing => {
    // 这里很容易出错,如果不把 `.then()` 返回的新实例赋给 `promise` 的话,就不是队列,而是批量执行
    promise = promise.then( () => {
      return new Promise( resolve => {
        doThing(thing, () => {
          resolve();
        });
      });
    });
  });
  return promise;
}

queue(['lots', 'of', 'things', ....]);

// 使用 Array.prototype.reduce
function queue(things) {
  return things.reduce( (promise, thing) => {
    return promise.then( () => {
      return new Promise( resolve => {
        doThing(thing, () => {
          resolve();
        });
      });
    });
  }, Promise.resolve());
}

queue(['lots', 'of', 'things', ....]);

这个例子如此直接我就不再详细解释了。下面我们看一个相对复杂的例子,假设需求:

开发一个爬虫,抓取某网站。

const spider = require('spider');

function fetchAll(urls) {
  return urls.reduce((promise, url) => {
    return promise.then( () => {
      return fetch(url);
    });
  }, Promise.resolve());
}
function fetch(url) {
  return spider.fetch(url)
    .then( content => {
      return saveOrOther(content);
    })
    .then( content => {
      let links = spider.findLinks(content);
      return fetchAll(links);
    });
}
let url = ['http://blog.meathill.com/'];
fetchAll(url);

这段代码,我假设有一个蜘蛛工具(spider)包含基本的抓取和分析功能,然后循环使用 fetchfetchAll 方法,不断分析抓取的页面,然后把页面当中所有的链接都加入到抓取队列当中。通过递归循环的方式,完成网站抓取。

Generator

如果你了解 Generator,你应该知道 Generator 可以在执行时中断,并等待被唤醒。如果能把它们连到一起使用应该不错。

let generator = function* (urls) {
  let loaded = [];
  while (urls.length > 0) {
    let url = urls.unshift();
    yield spider.fetch(url)
      .then( content => {
        loaded.push(url);
        return saveOrOther(content);
      })
      .then( content => {
        let links = spider.findLinks(content);
        links = _.without(links, loaded);
        urls = urls.concat(links);
      });
  }
  return 'over';
};

function fetch(urls) {
  let iterator = generator();

  function next() {
    let result = iterator.next();
    if (result.done) {
      return result.value;
    }
    let promise = iterator.next().value;
    promise.then(next);
  }

  next();
}

let urls = ['http://blog.meathill.com'];
fetch(urls);

Generator 可以把所有待抓取的 URL 都放到一个数组里,然后慢慢加载。从整体来看,暴露给外界的 fetch 函数其实变简单了很多。但是实现 Generator 本身有点费工夫,其中的利弊大家自己权衡吧。

小结

关于 Promise 的内容到此告一段落。这里我介绍了大部分的功能、函数和常见用法,有一些特殊情况会在后面继续说明。

results matching ""

    No results matching ""