[译]使用Promise

504 阅读11分钟

这是我参与更文挑战的第12天,活动详情查看:更文挑战

本文翻译自 《Working with Promises》

介绍

Promise的出现,提供了一种更好的方式来处理JavaScript中的异步代码。在此之前,已经有一些第三方库实现了类似功能,例如:

Q
when
WinJS
RSVP.js

上面这些Promise库,和ES2015 JavaScript规范(即ES6)也都支持了Promise

有关支持Promise的浏览器列表,可以参考: Can I Use

为什么要使用Promise

在JavaScript中,异步操作是很常见的,它可以用来请求网络资源或访问本地磁盘,和Web Worker、Service Worker通信,以及使用定时器等。

大多数情况下,当这些操作处理完成后,都是通过回调函数或事件来进行通信的。在简单网页的时代,这些方法可以运行得比较好,但是在一些大型的应用里效果就会不尽如人意。

旧方法: 使用事件

使用事件来报告异步结果,有很多明显的缺点:

  • 需要把代码放在各个事件处理器里,导致代码太过分散;
  • 在定义处理函数和接收事件之间,可能会出现临界状态导致紊乱;
  • 为了维持同步,常常需要定义一个类或者全局变量来维护状态。

这些问题使得异步处理变得复杂,不信可以随便找一个XMLHttpRequest的代码看看。

旧方法: 使用回调

另一个办法就是使用回调了,具有代表性的是使用一个匿名函数。看起来就像下面这样:

function isUserTooYoung(id, callback) {
  openDatabase(function(db) {
    getCollection(db, 'users', function(col) {
      find(col, {'id': id}, function(result) {
        result.filter(function(user) {
          callback(user.age < cutoffAge);
        });
      });
    });
  });
}

这种回调的方式也有两个问题:

  • 阅读理解复杂:使用的回调越多,嵌套就越深,代码就越不容易理解和分析;
  • 错误处理复杂:例如,其中一个函数接收到了非法参数,会怎么样?

使用Promise

Promise提供了一种标准化的方式去管理异步操作,以及处理异常。以刚才的代码为例,使用Promise可以更加简单:

function isUserTooYoung(id) {
  return openDatabase() // returns a promise
  .then(function(db) {return getCollection(db, 'users');})
  .then(function(col) {return find(col, {'id': id});})
  .then(function(user) {return user.age < cutoffAge;});
}

可以把Promise理解为一个等待异步操作结束的对象,然后依次调用下一个函数。我们可以通过调用.then()方法来传入下一个函数。当异步函数结束时,它的结果会传给Promise,然后Promise再传给下一个函数(作为参数)。

注意可能有很多次.then()的调用,每一次调用都会等待上一个Promise结束,再执行下一个函数,有必要的话再把结果返回给Promise。这样我们就可以无痛地链接同步和异步调用了。它极大地简化了我们的代码,所以现在大多数新的规范都会从异步方法中返回Promise

Promise 术语

在使用Promise时,我们常常会接触到和回调或者其他异步操作相关的术语。

在下面的例子中,我们需要把一个设置图片地址的异步任务转换成Promise

function loadImage(url) {
  // wrap image loading in a promise
  return new Promise(function(resolve, reject) {
    // A new promise is "pending"
    var image = new Image();
    image.src = url;
    image.onload = function() {
      // Resolving a promise changes its state to "fulfilled"
      // unless you resolve it with a rejected promise
      resolve(image);
    };
    image.onerror = function() {
      // Rejecting a promise changes its state to "rejected"
      reject(new Error('Could not load image at ' + url));
    };
  });
}

Promise有以下几种状态:

  • 进行中(Pending) - 异步操作还在进行中,Promise的结果还没有出来
  • 已完成(Fulfilled) - 异步操作已经结束,Promise获得了一个返回的值
  • 已终止(Rejected) - 异步操作失败,Promise无法完成,同时会返回一个失败的原因

除此之外还有另一个名词:settled,它指的是已执行的Promise,要么已成功(Fulfilled),要么已失败(Rejected)。

如何使用promise

写一个简单的promise

我们通过一个典型片段,来学习如何创建一个Promise

var promise = new Promise(function(resolve, reject) {
  // 做一些事情,异步操作等等
  if (/* 符合预期的结果 */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

Promise构造函数接收一个参数——包含两个参数的回调函数:resolve 和 reject。我们可以在回调函数中做各种事情,比如异步操作,如果一切正常就调用 resolve,否则调用 reject。

通过 reject 来处理错误的方式,有点像在普通 JavaScript 中常用的 throw 方法,但不是必须的。reject 出来的Error对象最大的好处是,能捕获跟踪堆栈,在调试的时候很有用。

创建好Promise后,我们来看一下如何使用它:

promise.then(function(result) {
  console.log("Success!", result); // "Stuff worked!"
}, function(err) {
  console.log("Failed!", err); // Error: "It broke"
});

.then()方法包含两个参数,一个是成功的回调,一个是失败的回调。二者都是可选的,你可以只添加其中一个。 更常见的方式是通过.then()来处理成功结果,用.catch()来处理错误。

promise.then(function(result) {
  console.log("Success!", result);
}).catch(function(error) {
  console.log("Failed!", error);
})

.catch()方法也很简单,相等于在 then 方法中只传失败的回调——then(undefined, func),不过可读性更强。需要注意上面两个例子执行的方式是不同的。 后者相当于:

promise.then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

它们的差别很微妙,但很有用。在Promise中如果出现错误需要 reject,当前then()方法中没有 reject 回调的话,它会依次到下一个then()方法中去寻找(或者在catch()中去寻找,因为它俩等效)。

上面第一个例子用的是then(func1, func2),func1 和 func2 二者只会被调用一个,不存在同时调用的情况。 但是在第二个例子then(func1).then(undefined, func2),和第三个例子then(func1).catch(func2)中,如果 func1 出现了错误需要 reject ,fun2 也会被调用,因为它们是执行链上的两个独立的步骤。

Promise链:then 和 catch

在一个Promise上,我们可以添加多个then()catch()来形成一个Promise链。在Promise链中,前一个函数返回的结果就成为了下一个函数接收的参数。

Then

then()方法接收函数参数来处理Promise的返回结果。当一个Promise成功返回,.then()会提取它返回的值(即Promise中 resolve 的值),然后执行回调函数,并且把这个返回值包装在一个新的Promise中。

可以把then()理解为try/catch块中try的部分。

记住我们之前的例子里,连续调用多个操作。

function isUserTooYoung(id) {
  return openDatabase() // returns a promise
  .then(function(db) {return getCollection(db, 'users');})
  .then(function(col) {return find(col, {'id': id});})
  .then(function(user) {return user.age < cutoffAge;});
}

第一个Promise中返回的值,会通过then()方法来传递。then()方法又会继续返回,可以返回一个Promise继续传递下去,也可以返回一个值,这个值就成为了后面函数的参数。通过then(),你可以链接任意数量的操作。

Catch

Promise 还提供了简单的错误处理机制,当一个Promise被 reject(或者抛出异常),它就会直接跳到第一个catch()的地方调用函数。

可以把catch前面的那些调用链看成被包裹在隐形的try{}方法中了。

在下面的例子里,我们通过loadImage()加载一张图片,然后用then()执行一系列转换操作。如果某个地方报错了(包括原始Promise以及后续任何步骤),它会直接跳转到catch()语句。

只有最后一段then()语句会把图片加入DOM,在那之前,我们都返回同样的图片,以便传递给下一个then()

function processImage(imageName, domNode) {
  // returns an image for the next step. The function called in
  // the return statement must also return the image.
  // The same is true in each step below.
  return loadImage(imageName)
  .then(function(image) {
    // returns an image for the next step.
    return scaleToFit(150, 225, image);
  })
  .then(function(image) {
    // returns the image for the next step.
    return watermark('Google Chrome', image);
  })
  .then(function(image) {
    // Attach the image to the DOM after all processing has been completed.
    // This step does not need to return in the function or here in the
    // .then() because we are not passing anything on
    showImage(image);
  })
  .catch(function(error) {
    console.log('We had a problem in running processImage', error);
  });
}

在一个Promise链里,我们还可以通过 catch 方法去做一些恢复处理。例如,下面的代码里,如果在loadImagescaleToFit阶段出了问题,我们就在 catch 方法中提供一个应急图片,继续传入后面的then方法中执行。

function processImage(imageName, domNode) {
  return loadImage(imageName)
  .then(function(image) {
    return scaleToFit(150, 225, image);
  })
  .catch(function(error) {
    console.log('Error in loadImage() or scaleToFit()', error);
    console.log('Using fallback image');
    return fallbackImage();
  })
  .then(function(image) {
    return watermark('Google Chrome', image);
  })
  .then(function(image) {
    showImage(image);
  })
  .catch(function(error) {
    console.log('We had a problem with watermark() or showImage()', error);
  });
}

注意: Promise链在执行完catch()语句后仍然会继续往下执行,直到执行完最后一个then()或者catch()才停止。

同步操作

Promise执行的函数里,并不是都需要返回一个Promise。如果函数是同步的,可以直接执行,就没必要返回一个Promise

下面的scaleToFit函数是图片处理链路上的一部分,它没有返回一个Promise

function scaleToFit(width, height, image) {
  image.width = width;
  image.height = height;
  console.log('Scaling image to ' + width + ' x ' + height);
  return image;
}

但是,这个函数需要把传入的图片返回,让图片可以继续在下一个函数中传递。

Promise.all

很多时候我们需要等一系列异步操作都成功完成之后,再执行某些动作。Promise.all会返回一个Promise,如果所有传入的Promise都完成了,就执行 resolve ;如果任何一个传入的Promise失败了,就执行 reject,同时会返回对应的错误原因。这对我们需要确保一组异步动作的完成后再进行下一步的情况非常有用。

在下面的例子里,promise1promise2 都返回Promise。我们希望在它们俩都加载完成后再继续。我们把两个Promise都传入到Promise.all中,如果任何一个请求失败了,Promise.all就会 reject 出错的Promise。如果两个请求都成功,Promise.all就会以数组形式 resolve 两个Promise的返回值。

var promise1 = getJSON('/users.json');
var promise2 = getJSON('/articles.json');
Promise.all([promise1, promise2]) // Array of promises to complete
.then(function(results) {
  console.log('all data has loaded');
})
.catch(function(error) {
  console.log('one or more requests have failed: ' + error);
});

注意: 即使其中一个Promise reject,导致整个Promise.all reject,剩下的Promise仍然会继续执行,只是不会通过Promise.all返回而已。

Promise.race

另一个会被用到使用的方法是Promise.racePromise.race同样接收一个Promise列表,然后当有一个Promise率先执行完成后,Promise.race也执行完成。如果最快的这个Promise resolve了,Promise.race就resolve相应的值,如果这个Promise reject了,Promise.race也以相应的原因 reject。

下面的代码展示了Promise.race的使用例子:

Promise.race([promise1, promise2])
.then(function(value) {
  console.log(value);
})
.catch(function(reason) {
  console.log(reason);
});

如果其中一个Promise先 resolve 了,就立即执行 then 代码块里的内容,并且把 resolve 的值记录下来。

如果其中一个Promise先 reject 了,就立即执行 catch 里面的内容,并且把原因记录下来。

顾名思义,Promise.race就是让Promise竞赛,谁先返回就用谁,这个看起来很吸引人,但也容易忽略一些问题,比如下面的例子:

var promise1 = new Promise(function(resolve, reject) {
  // something that fails
});
var promise2 = new Promise(function(resolve, reject) {
  // something that succeeds
});
Promise.race([promise1, promise2])
.then(function(value) {
  // Use whatever returns fastest
})
.catch(function(reason) {
  console.log(reason);
});

乍一看,这段代码似乎在让两个Promise比赛,一个 reject,另一个 resolve,然后看谁第一个返回就用谁。但是,如果其中一个Promise reject了,Promise.race会立即 reject,即使另一个Promise很快就可以 resolve 了也没用。

因此,如果promise1promise2之前 reject,即使promise2马上就会 resolve,Promise.race也会立刻 reject。Promise.race本身不能保证会返回第一个 resolve 的Promise

另一种有意思的用法是下面这种:

var promise1 = new Promise(function(resolve, reject) {
  // get a resource from the Cache
});
var promise2 = new Promise(function(resolve, reject) {
  // Fetch a resource from the network
});
Promise.race([promise1, promise2])
.then(function(resource) {
  // Use the fastest returned resource
})
.catch(function(reason) {
  console.log(reason);
});

这个例子看上去是想让网络资源和缓存资源去竞争,哪个先返回就用哪个。但是,Cache APIFetch API都可能返回一个不是我们想要的结果(fetch在404的情况下也会返回,caches在拿不到资源的时候也可能返回错误的资源)。在这个例子里,如果缓存里的资源不可用,但由于它通常返回得更快,Promise.race就会用它返回的错误结果去解析,并且忽略掉可能马上就获取到的网络资源。

有关Cache & network race的部分,请参阅Offline Cookbook