JavaScript 使用 Promise

本文最后更新于:2021年6月21日 中午

场景

为什么要使用 Promise?

JavaScript 异步发展史:回调函数 -> Promise -> async/await

传统异步使用回调函数,回调意味着嵌套,当你需要使用很多异步函数时,那你需要非常多的回调函数,可能形成回调地狱。
有问题就有人解决,js 没有多线程,所以天生就是异步的。正是因为异步的广泛性,所以很早之前就有人着力于解决异步回调的问题,github 上有很多已经废弃的库就是用于解决这个问题的。
然而现在,es6 出现了 Promise,它能把嵌套回调压平为一层的链式调用,并且写进了 js 标准里。es7 甚至出现了更加优雅的方式,async/await,能以同步的方式写异步的代码。当然,本质上只是 Promise 的一个语法糖,但其重要性也是不言而喻的——异步回调地狱已经不存在了!
说了这么多,那么平常我们应该怎么使用 Promise 呢?

使用 Promise

一般而言,我们作为使用者是无需创建 Promise 的,支持 Promise 的函数会返回一个 Promise 对象给我们,然后我们使用它的方法 then/catch 即可。

  • then():当前的 JavaScript 已经完成,要进行下一步的同步/异步操作了
  • catch():用于捕获 Promise 链式调用中可能出现的错误

注:then/catch 均返回一个新的 Promise

例如我们有这样一个需求

  1. 等待资源 A 加载完成
  2. 在 A 资源加载完成之后等待 B 资源加载完成

之前使用回调函数,我们的代码可能是这样的

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
33
/**
* 等待指定的时间/等待指定表达式成立
* @param {Number|Function} param 等待时间/等待条件
* @param {Function} callback 回调函数
*/
function wait(param, callback) {
if (typeof param === 'number') {
setTimeout(callback, param)
} else if (typeof param === 'function') {
var timer = setInterval(() => {
if (param()) {
clearInterval(timer)
callback()
}
}, 100)
} else {
callback()
}
}

wait(
() => document.querySelector('#a'),
() => {
wait(
() => document.querySelector('#b'),
() => {
console.log('a, b 两个资源已经全部加载完成')
},
)
},
)
// 结果
// a, b 两个资源已经全部加载完成

可以看到,上面如果还需要等待 c,d,e,f... 资源,那么回调函数的层级将是无法接受的。
现在,我们使用 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
31
32
// 先不要管这个函数的具体实现,下面再说如何自己封装 Promise
/**
* 等待指定的时间/等待指定表达式成立
* @param {Number|Function} param 等待时间/等待条件
* @returns {Promise} Promise 对象
*/
function wait(param) {
return new Promise((resolve) => {
if (typeof param === 'number') {
setTimeout(resolve, param)
} else if (typeof param === 'function') {
var timer = setInterval(() => {
if (param()) {
clearInterval(timer)
resolve()
}
}, 100)
} else {
resolve()
}
})
}

wait(() => document.querySelector('#a'))
// 注意这里的 wait(() => document.querySelector('#b')) 同样是一个异步函数,返回了一个 Promise
// 接下来,有趣的地方来了
// 很明显,wait 是一个异步函数。wait 函数的 then 函数调用了另一个异步函数,然而 then 会等待异步执行完成,才继续执行后面的函数
.then(() => wait(() => document.querySelector('#b')))
// 这里仍然会等待上面的 Promise 完成之后才执行下面的内容
.then(() => console.log('a, b 两个资源已经全部加载完成'))
// 结果
// a, b 两个资源已经全部加载完成

下面我们尝试使用一下 catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wait(() => document.querySelector('#a'))
.then(() => wait(() => document.querySelector('#b')))
.then(() => {
throw new Error('执行了某些操作发生了异常')
})
// 上面抛出了异常并且没有使用 catch 处理的话就会继续找下一个调用,直到找到处理的 catch,或者调用结束为止
.then(() => console.log('a, b 两个资源已经全部加载完成'))
// 捕获上面的 then() 发生的异常,保证后面的调用正常执行
.catch((error) => console.log('使用 catch 捕获的异常: ', error))
.then(() => console.log('测试异步函数结束'))

// 结果
// 使用 catch 捕获的异常: Error: 执行了某些操作发生了异常
// at wait.then.then (<anonymous>:4:11)
// VM272:9 测试异步函数结束

可以参考 MDN 上的教程 使用 Promises

封装 Promise

那么,你是否也对上面自定义的 wait 函数感到好奇呢?我们来详细的了解一下具体如何做到的吧!

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
/**
* 等待指定的时间/等待指定表达式成立
* @param {Number|Function} param 等待时间/等待条件
* @returns {Promise} Promise 对象
*/
function wait(param) {
// 这里返回了一个 Promise 对象,Promise 的构造函数要求一个函数参数
// 函数的参数实际上有两个,resolve 和 reject,分别代表 [已经完成] 和 [出现错误]
// 注:这个函数是立刻执行的,当 resolve 或 reject 执行时,这个 Promise 算是结束了,将进入下一个 then/catch 调用
return new Promise((resolve) => {
if (typeof param === 'number') {
setTimeout(resolve, param)
} else if (typeof param === 'function') {
var timer = setInterval(() => {
if (param()) {
clearInterval(timer)
// 这里执行了代码,如果有什么结果需要传递给下一个调用,则直接放到 resolve 函数内即可
resolve()
}
}, 100)
} else {
resolve()
}
})
}

同样的,我们也可以使用 Promise 封装其他函数

  • timeout:一个简单的 setTimeout() 的封装
  • readLocal:读取本地浏览器选择的文件
  • timing:测试函数执行的时间,不管是同步还是异步的(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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 使用 Promise 简单封装 setTimeout
* @param {Number} ms 等待时间
* @returns {Promise} Promise 对象
*/
const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
/**
* 读取本地浏览器选择的文件
* @param {File} file 选择的文件
* @param {{String}} init 一些初始选项,目前只有 type 一项
* @returns {Promise} 返回了读取到的内容(异步)
*/
const readLocal = (() => {
const result = (file, { type = 'readAsDataURL' } = {}) =>
new Promise((resolve, reject) => {
if (!file) {
reject('file not exists')
}
const fr = new FileReader()
fr.onload = (event) => {
resolve(event.target.result)
}
fr.onerror = (error) => {
reject(error)
}
fr[type](file)
})
result.DataURL = 'readAsDataURL'
result.Text = 'readAsText'
result.BinaryString = 'readAsBinaryString'
result.ArrayBuffer = 'readAsArrayBuffer'
return result
})()

/**
* 测试函数的执行时间
* 注:如果函数返回 Promise,则该函数也会返回 Promise,否则直接返回执行时间
* @param {Function} fn 需要测试的函数
* @returns {Number|Promise} 执行的毫秒数
*/
function timing(fn) {
const begin = performance.now()
const result = fn()
if (!(result instanceof Promise)) {
return performance.now() - begin
}
return result.then(() => performance.now() - begin)
}

吾辈建议你也可以封装一些常用的异步函数,下面会展示 JavaScript 中如何更简单的使用异步!

使用 async/await

  • async:用于标识一个函数是异步函数,默认这个函数将返回一个 Promise 对象
  • await:用于在 async 函数内部使用的关键字,标识一个返回 Promise 的异步函数需要等待

使用 async/await 重构上面的代码

1
2
3
4
5
6
7
8
async function init() {
// await 等待异步函数执行完成
await wait(() => document.querySelector('#a'))
await wait(() => document.querySelector('#b'))
console.log('a, b 两个资源已经全部加载完成')
}
// 注:init() 函数将返回一个 Promise,我们可以继续追加下一步的操作
init()

是的,就是如此简单,直接在异步函数添加 await 关键字就好了!


最后,如果你要使用这些特性,请务必使用 babel 转换器。毕竟,有太多的人就是不肯升级浏览器。。。

可以参考


JavaScript 使用 Promise
https://blog.rxliuli.com/p/f7d449caae624527b5ed6fa00031b164/
作者
rxliuli
发布于
2020年2月2日
许可协议