[译]使用JavaScript Promise的优点

1,198 阅读3分钟

本文译自@Mohsan Riaz发表在medium的文章,已获其授权,原文地址见传送门

在Javascript中编写异步代码常常令人不安,尤其是在使用continuation-passing style(CPS)风格的时候。异步代码影响代码的可读性,使代码很难解耦为更小的独立代码块,导致其更易出错,且以后很难改动。

译者注:continuation-passing style,简单来说就是函数的返回不依靠return语句,而是依靠额外传入一个回调函数,将返回值作为回调函数的参数来传出。具体可参考这篇博客

如果有一种可以以类似同步代码的风格来写异步代码的方式,那我们的编程生涯肯定会更加的愉快。使用JavaScript的promise可以做到这一点。

让我们先开始尝试找找使用callback有什么问题。

Callbacks

例如:如果需要获取一个国家的列表,紧接着获取国家列表中第一个国家的的城市列表,然后紧接着获取这个城市列表中的所有大学,最终展示第一个城市的第一所大学。

由于每次调用都相互依赖于上一次调用的结果,需要调用一系列的嵌套的回调函数。

function fetchCountries(){
  fetchJSON("/countries", (success, countries) => {
    if(success){
      try{
        // do some stuff
        fetchCities(countries[0].id);
      } catch(e){
          processError();
      }
    }else
      processError();
  });
}

function fetchCities(countryId){
  fetchJSON(`countries/${countryId}/cities`, (success, cities) => {
    if(success){
      try{
        // do some stuff
        fetchUniversities(cities[0].id);
      } catch(e){
          processError()
      }
    }else
      processError();
  });
}

function fetchUniversities(cityId){
  fetchJSON(`cities/${cityId}/universities`, (success, universities) => {
    if(success){
      try{
        // do some stuff
        fetchUniversityDetails(universities[0].id);
      }catch(e){
        processError();
      }
    }else
      processError();
  });
}

function fetchUniversityDetails(univId){
  fetchJSON(`/universities/${univId}`, (success, university) => {
    if(success){
      try{
        // do some stuff
        hideLoader();
      }catch(e){
        processError();
      }
    }else
      processError();
    
  });
}

上面这个实现有什么问题?

回调函数没有将异步逻辑放在同一个位置,而是决策接下来调用哪个函数。简而言之,我们将控制流给了各个函数,让他们紧密的耦合到了一起。

为什么用Promise

在我看来,与简单的回调处理(continuation-passing style)相比,promise有4个主要优点:

  1. 更好的定义异步逻辑控制流
  2. 解耦
  3. 更好的错误处理
  4. 提升可读性

Promise的使用

Javascript的Promise让异步代码像同步代码一样return一个值,这个返回值是个能承诺成功或失败值的object。这个小改动让它用起来非常强大。

get("/countries")
  .then(populateContDropdown)
  .then(countries =>  get(`countries/${countries[0].id}/cities`))
  .then(populateCitiesDropdown)
  .then(cities => get(`cities/${cities[0].id}/universities`))
  .then(populateUnivDropdown)
  .then(universities => get(`universities/${universities[0].id}`))
  .then(populateUnivDetails)
  .catch(showError)
  .then(hideLoader)

与同步编程相似,一个函数的输出是下一个函数的输入,在调用链中像使用JSON.parse一样使用JS函数,他们的返回值会被喂给下一个回调函数。

类似JavaScript的try/catch,如果有异常(exception),catch函数会将其捕获,后面的代码会照常运行,上面例子中的loader会在执行后隐藏。

对比

我们在同一个地方定义所有异步逻辑,不必做额外的检查或者为错误处理使用try/catch。最终,代码具有更高的可读性,更低的耦合度,可复用的独立函数。

错误处理的细节

无论何时出现有意或无意的异常,都会在下一个catch处理程序中抛出异常。你可以将catch处理程序放置在链中的任何位置以捕获特定的异常,此时可以显示一个错误或者重新取值。

get("/countries")
  .then(JSON.parse)
  .catch(e => console.log("Could not parse string"))
  .then(...)
  .catch(e => console.log("Error occured"))

在上面的例子中,第二个console语句将不会被打印。

如果一个异常已被捕获且想将其传到下一个catch处理函数中,必须将其抛出。

如果想展示多个错误信息这样就很方便,例如:

get("/coutries")
  .then(...)
  .catch(e => {
    showLowLevelError(); 
    throw e;
  })
  .catch(showHighLevelError)

Promise vs Event listener

如果一个事件希望能被多次触发,事件监听器就非常适用,例如按钮点击事件。Promise和事件监听器很相似,但他们有两点本质的不同:

  1. 无论是异常还是正常值Promise都只会决议一次。
  2. 即使在Promise决议后添加的回调函数任然会被调用,这意味着可以稍后检查Promise是fullfiled还是rejected

这很有用,因为我们对发生的结果做出反应更感兴趣。举个图片预加载的简单例子:

var imagePromise = preloadImage("src.png");
setTimeout(() => {
  imagePromise.then(() => console.log("image was loaded") )
     .catch(()=> console.log("could not load the image"))
}, 2000)

function preloadImage (path) {
  return new Promise((resolve, reject) => {
    var image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

Promise.all([...])对于批量决议很有用

Promise.all([imagePromise1, imagePromise2, ....])
  .then(...)
    .catch(...)

总结

在JavaScript中的大部分实践中,Promise非常有用,尤其是成功或者失败回调函数只能被执行一次。但在一些实践中,尤其是事件回调函数会被多次调用的实践中,普通的回调函数更好。感谢你阅读这篇文章,如果有什么问题,请在评论区留言。

初次翻译,如有问题欢迎指正。