理解 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