本文最后更新于: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
例如我们有这样一个需求
- 等待资源 A 加载完成
- 在 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
|
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 两个资源已经全部加载完成') }, ) }, )
|
可以看到,上面如果还需要等待 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
|
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')) .then(() => wait(() => document.querySelector('#b'))) .then(() => console.log('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('执行了某些操作发生了异常') }) .then(() => console.log('a, b 两个资源已经全部加载完成')) .catch((error) => console.log('使用 catch 捕获的异常: ', error)) .then(() => console.log('测试异步函数结束'))
|
可以参考 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
|
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() } }) }
|
同样的,我们也可以使用 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
|
const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
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 })()
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 wait(() => document.querySelector('#a')) await wait(() => document.querySelector('#b')) console.log('a, b 两个资源已经全部加载完成') }
init()
|
是的,就是如此简单,直接在异步函数添加 await
关键字就好了!
最后,如果你要使用这些特性,请务必使用 babel 转换器。毕竟,有太多的人就是不肯升级浏览器。。。
可以参考