Promise - 来自Javascript的承诺
因
如果你和我一样,刚开始编程的时候学习的并非Javascript,而是类似于Java / C# / C++这些类似的语言。那么你在初写js(或泛前端应用)的时候,应该会和我遇到相似的疑问。那就是,我期望的代码执行顺序为什么和实际上的代码执行顺序不相同呢?
// Javascript
setTimeout(function(){
console.log(1)
}, 1000)
console.log(2)
// 期望输出: 1 2
// 实际输出: 2 1
我们知道,程序在读文件的时候是一行一行的读取的。(别问为啥,你也可以写个解释器先读取2n行,再读2n-1行) 然而js中的表现却和我们的预期不一致,做个对比,我们拿Python重复一遍上述的逻辑。
# Python
time.sleep(1)
print(1)
print(2)
# 实际输出:1 2
Python的逻辑符合我们的“直觉”,即代码总是按照码农给定的顺序执行。
但为啥就你js搞特殊?因为早期的js主要运行在浏览器,负责的工作则是对用户的输入(鼠标点击、移动、悬浮或键盘事件)进行反馈。注定了其代码不能是同步执行的,而是监听式的,即代码块在等待一个时机被执行,而这个执行时机程序员大概率是无法预知的。
虽然一个异步程序的执行时机无法预知,但我们可以使用回调提前对数据进行处理。例如:
function doSomethingFeature (callback) {
... // 假设执行这一步需要花费一个不确定的时间
if (!args) args = {}
if (callback) callback(args)
}
doSomethingFeature(function(args) {
// 在这里可以对数据进行处理
console.log(args)
})
回调地狱
我们用回调函数来处理一个个事件,于是代码中的括号也越来越多。
- 鲁迅
想象一下:网页中的一个弹窗,它有一个按钮,在这个按钮被按下时,浏览器开始向远程服务器获取数据,当数据到达后,网页中的标题变了。由于每一次发生的事件都要依赖于上一次事件(因为不触发事件就无法获取数据), 用回调函数进行数据处理就会变成:
openCustomWindow(()=>{
const button = document.createElement('button')
button.onClick = () => {
api.get('/data', (data)=> {
const title = document.getElementById('title')
title.content = data
})
}
})
这样的代码,不仅难以阅读,而且维护时多一个括号少一个括号的也很容易让人心态崩溃,只能用用编辑器的智能提示勉强混混这样子。
链式调用
那如果有一种写法,可以让被依赖的数据像自来水管流动一样就好了。按照顺序地从自然水源中经由水厂消毒,在流向小区供水中心,再流向你家的净水器,最后从水龙头流入你的杯子里。 把上面的列子用链式调用的方案改写一下,就得到一个类似于下面的东西:
// js伪代码:理想状态的链式调用模式
openCustomWindow()
.whenButtonClicked()
.apiGet('/data')
.changeTitle()
2003年JQuery横空出世,当时的JQuery就采用了这种做法,从而大大地提高了前端代码的可读性,加之提供完整的类库功能和简单的学习曲线,使得JQuery成为了几乎“人手必会”的前端类库。而受到JQuery的启发地ES委员会,创造性地在Javascript中引入了Promise对象。
Promise字面意义上代表承诺,在现实生活中,一个承诺也会有履行完成的状态和未履行的状态。因此,Promise对象也可以处理程序成功和失败后的数据。一个典型的Promise写法如下:
function doSomething() {
return new Promise((resolve, reject)=>{
try {
... // do something
resolve() // 标记为已解决
}catch(e) {
reject(e) // 标记为为完成
}
})
}
doSomething().then(()=>{
// resolve() 被调用后,执行此方法
...
}).catch((e)=>{
// reject() 被调用后,执行此函数
console.error(e)
})
更好用的Promise: async / await
Promise除了擅长链式调用以外,还可以搭配关键字await写出更易读的同步式代码。
function doSomething() {
return new Promise((resolve, reject)=>{
try {
... // do something
if(!args) args = {}
resolve(args) // 标记为已解决
}catch(e) {
reject(e) // 标记为为完成
}
})
}
try {
const args = await doSomething()
}catch(e) {
console.error(e)
}
// 也可以用下面这种写法
const args = await doSomething().catch((e)=>{
console.error(e)
return 1 // 当发生错误时,args的值为1
})