为什么应该使用 Promise

JavaScript 中的异步编程一直都是合格的 JS 程序员的基本修养。自从去年还是前年开始使用 Promise 以来,现在的感觉就是没 Promise 都快不会写 JS 了。

为什么应该用 Promise,我认为应该有这三个:

  • 代码看起来有线性感,更加的直观
  • 有个良好的异常处理机制
  • 它是 asyc/await 里一个很关键的东西

并且现在 Promise 的浏览器支持度越来越好了,就算不支持,也可以使用 Promise 类库。

不用「回调地狱」

过去编写 JavaScript 的异步流程,会用上回调函数的形式。

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log('two')
}, 16)
console.log('one')
// console output:
// one
// two

如果只是一两层的话,那就挺好的。但真实情况并非如此,为了保证异步流程的顺序,它往往会嵌套很多层,就像这样:

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log('已经过去了三秒')
setTimeout(() => {
console.log('我距离前面的 log 二秒后出现 ')
setTimeout(() => {
console.log('我距离前面的 log 一秒后出现')
}, 1000)
}, 2000)
}, 3000)

如果有更多的流程,那么它嵌套的就会越深入。更夸张的,还有这样的:

图片源

而 Promise 是「扁平」[1]的,要做到前面的效果这样写就行了:

1
2
3
4
5
6
7
8
9
10
11
12
const wait = t => new Promise(res => setTimeout(res, t))
wait(3000).then(() => {
console.log('已经过去了三秒')
return wait(2000)
}).then(() => {
console.log('我距离前面的 log 二秒后出现 ')
return wait(1000)
}).then(() => {
console.log('我距离前面的 log 一秒后出现')
})

这样写的好处就是有种线性感,这样对于理清异步流程的顺序是很有帮助的。

错误处理

错误处理是软件工程中需要考虑的环节。好的错误处理能够更快的发现错误、并且可以防止程序出现「闪退」的情况。

JS 是有个內建的错误处理机制,然而在异步代码下它是没有效果[2]的。其中一种方式是这样的形式:

1
2
3
4
5
6
7
8
9
10
const fs = require('fs')
fs.readFile('config.suc', (err, res) => {
if (err) {
console.warn('错误: ', err)
} else {
console.log('已读取:', res.toString())
}
})

这种错误处理的流程,得写在回调函数中的开头,判断是否有错误存在[3],如果有错误,则做一些处理。

这种形式看起来是挺好的,挺简单的形式。但还是那句话,如果异步流程很多的时候怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 假设是一个游戏的加载程序
const base = require('./lib/base-platform')
const DataLoader = require('./lib/data-loader')
const DataProcess = require('./lib/data-process')
const err_detect = err => {
if (err) {
base.gameError('游戏读取失败, 请检查文件完整', err)
throw err // 使用 throw 卡住后续代码的执行
}
}
DataLoader('game_ui', (err, game_ui_data) => {
err_detect(err)
DataLoader('character', (err, character_data) => {
err_detect(err)
DataLoader('effect', (err, effect_data) => {
err_detect(err)
DataLoader('map', (err, map_data) => {
err_detect(err)
const query = [game_ui_data, character_data, effect_data, map_data]
DataProcess(query, (err, resource) => {
err_detect(err)
base.enterGameHome(resource)
})
})
})
})
})

有没有觉得一排排的 err_detect 很烦人[4]?如果是 Promise 的话,只写一次就够了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const base = require('./lib/base-platform-promise')
const DataLoader = require('./lib/data-loader-promise')
const DataProcess = require('./lib/data-process-promise')
const load = () => {
cosnt query = []
return DataLoader('game_ui').then(game_ui_data => {
query.push(game_ui_data)
return DataLoader('character')
}).then(character_data => {
query.push(character_data)
return DataLoader('effect')
}).then(effect_data => {
query.push(effect_data)
return DataLoader('map')
}).then(map_data => {
query.push(map_data)
return query
})
}
load().then(query => {
return DataProcess(query)
}).then(resource => {
base.enterGameHome(resource)
}).catch(err => {
base.gameError('游戏读取失败, 请检查文件完整', err)
})

Promise 的线性结构和它的错误处理机制使得代码看上去比前者是要好理解的[5]

async/await 的前置技能

ES7 里的 async/await 可以说是 JS 异步编程的终极处理方案,它提供了以写同步代码的方式去写异步代码的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
const wait = t => new Promise(res => setTimeout(res, t))
(async () => {
await wait(3000)
console.log('已经过去了三秒')
await wait(2000)
console.log('我距离前面的 log 二秒后出现 ')
await wait(1000)
console.log('我距离前面的 log 一秒后出现')
})()

可以看到,async/await 是基于 Promise 的。await 后接上一个 Promise 对象的话,则会被「卡住[6]」直到这个 Promise 对象不再是 pedding 状态[7]而解开。若这个 Promise 对象是 resolved 的状态,则 await [Promise对象] 语句会返回 Promise 对象 resolved 的值[8]。若这个 Promise 对象是 rejected 状态,则会像同步代码才能使用的內建错误处理机制那样抛出一个错误,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const base = require('./lib/base-platform-promise')
const DataLoader = require('./lib/data-loader-promise')
const DataProcess = require('./lib/data-process-promise')
const load = async () => {
const query = []
query.push(await DataLoader('game_ui'))
query.push(await DataLoader('character'))
query.push(await DataLoader('effect'))
query.push(await DataLoader('map'))
return query
}
(async () => {
try {
const query = await load()
base.enterGameHome(await DataProcess(query))
} catch (err) {
base.gameError('游戏读取失败, 请检查文件完整', err)
}
})()

我们可以看到。async 函数执行后的返回值总是 Promise 对象[9]。若 async 函数在执行中抛出了一个错误[10]并且未捕获的话,作为返回值的 Promise 对象则会是 rejected 的状态[11];若 async 函数在执行中没有抛出错误,作为返回值的 Promise 对象则会是 resolved 的状态[12]

所以,想要用好 async/await,就必须了解 Promise


  1. 虽然 Promise 也可以写成地狱的形式,但是太傻比了,应该没人这样写 ↩︎

  2. 至少回调函数的观念是这样 ↩︎

  3. 若 err 是 null 的话则没有错误,有错误则是个 Error 对象 ↩︎

  4. 如果没有复用 if (err) {} 的话人可能会神经病了 ↩︎

  5. 并且处理逻辑里没有错误处理的代码,有错误会抛到 catch 上 ↩︎

  6. 但这卡住并不是 while(1) {} 这样的卡住,只是这段代码(或者说这个 async 函数)暂停执行了 ↩︎

  7. 就是 Promise 中的 resolve 或者 reject 执行了 ↩︎

  8. 这种情况的 await [Promise 对象] 等效于 [Prmoise 对象 resolve 的值] ↩︎

  9. 要像同步代码一样走到最后,await 要全部「卡」完,这个 Promise 才会取消 pedding 状态 ↩︎

  10. 不管是同步代码的 throw 还是 await 出现的错误 ↩︎

  11. 假设抛出的错误对象为 err,作为返回值的 Promise 对象等效于 Promise.reject(err) ↩︎

  12. 假设 return 返回值为 a,作为返回值的 Promise 对象等效于 Promise.resolve(a),没有 return 返回值的话则是 undefined ↩︎