理解 Promise (WIP)

一直想写篇文章帮助初学者理解 Promise。Promise 这个东西总是被人和异步回调这些东西联系在一起理解,而异步回调这些东西本身就已经很难理解了,再在上面叠上一个 Promise 理解起来更是难上加难。所以我考虑用另一种形式形式来表达 Promise 的特点,希望能对理解 Promise 有所帮助。

下面开始:

一、用 Promise 处理值

假如我们现在有一个数字:

4

假如我们又有一个操作叫 加五:

function add_five(n) { return n + 5 }

假如我们还有一个操作叫 开平方:

function square_root(n) { return Math.sqrt(n) }

最后我们也有一个操作叫输出:

function print(n) { console.log(n) }

那么,我们希望用一个表达式输出 4 加五再开平方,用流程图表示就是

     +----------+    +-------------+    +-------+
4 -> | add_five | -> | square_root | -> | print |
     +----------+    +-------------+    +-------+

写成 JavaScript 就是

print(square_root(add_five(4)))

用 Promise 的话,会清晰得多:

Promise.resolve(4)
  .then(add_five)
  .then(square_root)
  .then(print) // => 3

首先我们将 4 改成了 Promise.resolve(4),将数字 4 转成了返回 4 的 Promise 对象,然后我们就可以使用 then 来进行值的传递了。

在 Promise 的世界里面:Promise 对象相当于它返回的值。
在 Promise 的世界里面:Promise 对象相当于它返回的值。
在 Promise 的世界里面:Promise 对象相当于它返回的值。

二、出错了怎么办

我们用另一个(危险的)数字

-6

来做相同的操作,就需要加上错误处理了

try {
  print(square_root(add_five(-6)))
} catch (e) {
  print(e)
}

用 Promise 怎么做错误处理?

Promise.resolve(-6)
  .then(add_five)
  .then(square_root)
  .then(print) // 这一句会被跳过,不执行
  .catch(print) // => Error

catch 代替 then来捕获错误,没有什么本质区别

三、当时给不了结果怎么办

这个时候我们只要用返回 Promise 对象来代替直接返回值就好了。

现在我们来做一个操作叫 慢慢地加五:

function add_five_slowly(n) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(n + 5)
    }, 1000)
  })
}

和刚才的立即加五对比一下

function add_five(n) {
  return n + 5
}

事实上就是把 n + 5 替换成了一个返回 n + 5 的 Promise 对象

new Promise(function (resolve) {
  setTimeout(function () {
    resolve(n + 5)
  }, 1000)
})

在 Promise 的构造函数里面,我们使用 resolve 代替 return 来返回值。

这个时候

在 Promise 的世界里面:Promise 对象相当于它返回的值。
在 Promise 的世界里面:Promise 对象相当于它返回的值。
在 Promise 的世界里面:Promise 对象相当于它返回的值。

这样我们就可以像使用立即加五一样的方法使用慢慢地加五

Promise.resolve(4)
  .then(add_five_slowly)
  .then(square_root)
  .then(print) // => 3

我们就这样使用 Promise 轻松地统一了同步操作和异步操作!

类似,我们还可以做一个 慢慢地开平方

function square_root_slowly(n) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Math.sqrt(n))
    }, 2000)
  })
}

看起来没什么问题,但是当我们使用危险数字 -6 来执行慢慢开平方

Promise.resolve(-6)
  .then(add_five_slowly)
  .then(square_root_slowly)
  .then(print)
  .catch(print)

我们发现负数开平方的 Error 并没有被 catch 住,因为我们的错误是发生在异步环境里面,所以我们并不能简单的抛出异常。

在 Promise 的构造函数里面,使用第二个参数 reject 来抛出异常。

function square_root_safely_slowly(n) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      try {
        resolve(Math.sqrt(n))
      } catch (e) {
        reject(e)
      }
    }, 2000)
  })
}

这时候我们就可以正常地使用 catch 来捕获 Promise 中发生的错误了

Promise.resolve(-6)
  .then(add_five_slowly)
  .then(square_root_safely_slowly)
  .then(print)
  .catch(print) // => Error

四,并行计算

当我们连续使用多个 Promise 进行计算的时候

Promise.resolve(4)
  .then(add_five_slowly)
  .then(square_root_safely_slowly)
  .then(print) // => 3
Promise.resolve(-1)
  .then(add_five_slowly)
  .then(square_root_safely_slowly)
  .then(print) // => 2

我们会发现,三秒钟后同时出现了这两个计算的结果。

Promise 是强制异步进行的,下一个语句并不等待前一个语句执行完毕才执行。

所以我们可以随意指定几个 Promise 同时执行,并分别处理它们的执行结果。

如果我们需要一起处理它们的结果,要怎么写呢?我们首先使用传统的写法处理它们

var results = []
Promise.resolve(4)
  .then(add_five_slowly)
  .then(square_root_safely_slowly)
  .then(function (n) {
      results[0] = n
      if (results[1] != null) print(results[0] + results[1]) // => 5
  })
Promise.resolve(-1)
  .then(add_five_slowly)
  .then(square_root_safely_slowly)
  .then(function (n) {
      results[1] = n
      if (results[0] != null) print(results[0] + results[1]) // => 5
  })

使用 Promise.all 可以简单且直观地处理它们

var promise1 = Promise.resolve(4)
  .then(add_five_slowly)
  .then(square_root_safely_slowly)
var promise2 = Promise.resolve(-1)
  .then(add_five_slowly)
  .then(square_root_safely_slowly)

Promise.all([promise1, promise2])
  .then(function (results) {
    print(results[0] + results[1]) // => 5
  })

如果任何一个 Promise 出错了就走 catch,无论其它。

另外一个有用的方法是 Promise.race 一旦这一堆里面的第一个结果返回了结果,就会返回这个结果(也不知道是谁返回的),其它结果就都忽略了。

var promise1 = Promise.resolve(4)
  .then(add_five_slowly) // 这个要一秒
var promise2 = Promise.resolve(4)
  .then(square_root_safely_slowly) // 这个要两秒

Promise.race([promise1, promise2])
  .then(function (fastest_result) {
    print(fastest_result) // => 9
  })

如果第一个 Promise 就出错了就走 catch,仍然无论其它。

五,杂项

这里列出一些不影响理解的 Promise 杂项

  • Promise.resolve(4) 类似,Promise.reject(new Error('boom')) 是类似构造的简写形式

      new Promise(function (resolve, reject) {
        reject(new Error('boom'))
      })
    
  • 在 Promise 的构造函数里面,可以同步使用 throw 来触发错误,也就是说刚才那个语句块也可以写成

      new Promise(function () {
        throw new Error('boom')
      })
    

    **异步不行!**要是分不清楚同步和异步,就不要玩这个火了。。

  • 返回一个 Promise,相当于返回这个 Promise 的值。

      new Promise(function (resolve) {
        var promise = Promise.resolve(4)
          .then(add_five_slowly)
          .then(square_root_safely_slowly)
        resolve(promise)
      }).then(print) // => 3
    

    如果返回的这个 Promise 被 reject 了,返回就变成抛异常了。。

      new Promise(function (resolve) {
        var promise = Promise.resolve(-6)
          .then(add_five_slowly)
          .then(square_root_safely_slowly)
        resolve(promise)
      }).then(print)
        .catch(print) // => Error
    

    所以不要看到 resolve 就以为一定能拿到值,要看到 Promise 真真切切地返回的是才能确定。怎么理解呢,好比我们在同步代码里看到执行到 return 也并不能确定它就一定不会抛错一样,说不定人家写的是 return 2333/0。。

六、小技巧

WIP