2021 年 11 月 23 日更新。最近突然意识到,Promise 是一个 Monad,其 then 方法兼具 fmap 和 bind 的功能——返回值为新的 Promise 对象,或是为普通的值,都能正确处理,所谓的 await,async 语法很像 Haskell 中的 do。比如下面两个 asyncFunc 就是等价的。
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 function fun1 ( ) { return new Promise ((resolve, reject )=> { setTimeout (()=> {resolve (100 )}, 1000 ) }) }function fun2 ( ) { return new Promise ((resolve, reject )=> { setTimeout (()=> {resolve (100 )}, 1000 ) }) }async function asyncFunc1 ( ) { const numA = await fun1 () console .log ("numA:" + numA) const numB = await fun2 () console .log ("numB:" + numA) return numA + numB; }function asyncFunc2 ( ) { return fun1 ().then (numA => { console .log ("numA:" + numA) return fun2 ().then (numB => { console .log ("numB:" + numA) return numA + numB; }) }) }
promise 是 ES6 为解决异步编程过于复杂的问题所提出的一种技术/异步模式。对其的学习是必要的。
什么是 promise promise,意为“承诺”,它归根结底是一个类,其将异步操作(或同步操作)封装在自己的构造器中并获取结果或错误。其后其允许使用异步方法取出结果。
promise 的优点在于其能避免回调地狱——多层的嵌套,将嵌套调用变成链式调用,且其保存的结果将被缓存以备无数次的监听。其也提供了合适的方式以方便错误和异常的处理。promise 当然也有缺点——不然也不会有像 RxJS 这样其它的异步解决方案了,但是这是我当前感觉不到的。
Promise 构造器和 then,catch 使用 Promise 的一个比较典型的方式是使用其构造函数,其构造函数接受一个参数——它称为执行器,executor,该执行器有两个参数——resolve 和 reject,其都是函数,其作用为收集结果或错误,用户将异步操作置于其函数体中,并通过 resolve 收集结果,或通过 reject 收集错误,如果遇到异常,则效果同 reject 方法相同。整个函数体将会同步执行 。
Promise 对象维护两个属性——其一是其状态(state),状态分为三种——pending,resolve(fulfilled),reject。pending 是默认状态,标识还未获得结果。在调用 resolve 或 reject 时,状态将发生转移——从 pending 到 resolve,从 pending 到 reject,这种状态转移只会执行一次,也就是说它只会收集第一次调用 resolve 或 reject 的结果,其后的调用将被忽视(但是函数的执行不会被中断);另一个属性则是结果。
then 用户可以使用 then 方法,通过其回调接受 promise 的结果,then 方法是可以无数次执行的,其回调是异步的。下面是一个示例。then 也可以接受第二个函数参数——代表 reject 时的回调。可以认为 then 是在这里注册了一个临时的监听器,在状态为 fulfilled 时执行函数体,因此它是异步的,即使状态早在 then 方法执行前改变也是如此。
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 const p = new Promise <string >((resolve, reject ) => { console .log ("进入执行器函数体了" ) setTimeout (()=> { resolve ("hello, happy world!" ) console .log ("resolve 了" ) }, 1000 ) })console .log ("退出执行器函数体了" ) p.then ((v )=> { console .log ("获取到了" ) console .log (v) })console .log ("then 方法之后" )setTimeout (()=> { p.then (v => { console .log ("能够多次获取,结束后能继续获取" ) console .log (v) }).catch (console .log ) }, 2000 )
then 方法的返回值也是 promise 对象 (这或许是 promise 相比与原先的以回调为基础的异步编程的最大的差别吧?通过这种性质,then 方法能够对多个同步/异步任务进行串联,链式调用),其结果取决于 then 方法中函数的返回值(如果成功,调用 onfulfilled,如果失败,调用 onrejected)。函数正常返回时,返回一个结果为返回值的 resolved 的 promise 对象;函数抛出异常时,返回一个结果为异常的 rejected 的 promise 对象;函数返回新的 promise,则返回值就为该 promise。
const p = Promise .resolve (1 )let res = p.then ((v )=> { return 1000 ; })setTimeout (()=> { console .log (res) }, 0 )
利用 then 的这种特性,可以进行链式调用。
Promise .resolve () .then (()=> { console .log (1 ); }) .then (()=> { console .log (2 ); }) .then (()=> { console .log (3 ); })
可以通过返回一个永远在 pending 状态的 promise 来中断调用链,这是中断的唯一办法(异常不能算中断,它进入了错误处理逻辑中)。
Promise .resolve () .then (()=> { console .log (1 ); return new Promise (()=> {}) }) .then (()=> { console .log (2 ); }) .then (()=> { console .log (3 ); })
进行链式调用时,必须要遵循这样的规范——同步调用直接写在方法的参数的函数体中,异步调用写在新的 promise 对象的执行器中。
catch 和异常穿透 catch 方法接受这样一个函数参数——它在 reject 时执行。乍得一看 catch 好似是 then 的子集,没有意义,但是 catch 有这样一个特性——异常穿透 ,当进行 then 的链式调用时,链条上抛出的异常/错误会直接到达 catch,并且错误提示中能够指出错误发生的具体位置!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Promise .resolve () .then (()=> { console .log (1 ); }) .then (()=> { throw new Error ("err!" ) console .log (2 ); }) .then (()=> { console .log (3 ); }) .catch (err => {console .warn (err)})
一个简单的实现 下面展示了对 Promise 的一个简单的实现。其中 then 的实现是比较重要的——它将返回一个新的 Promise,在执行器中,它将等待直到当前 Promise 实例的状态不为 pending,然后修改自身的状态。这里可以有其它的实现方法——比如在 Promise 中维护一个保存 then 中回调的数组作为属性,调用 then 方法时,如果 Promise 仍旧是 pending 状态,则将这时的上下文添加到数组中,在状态改变时,即调用 resolved,reject 方法时,对数组进行遍历执行。如果不是 pending,则直接执行(但应当通过 setTimeout 将其变为异步)。同时关于 result 是 promise 时进行处理的逻辑也转移到 then 方法中执行。
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 enum PromiseState { PENDING , FULFILLED , REJECTED }function waitUntil (doSomething:()=>void ,signal:()=>boolean ) { const interval = setInterval (()=> { if (signal ()) { doSomething (); clearInterval (interval) return ; } }) }class MyPromise <T> { private state : PromiseState = PromiseState .PENDING private value : any = null private setToPromise (anotherMyPromise : MyPromise<T> ) { waitUntil (()=> { this .value = anotherMyPromise.value this .state = anotherMyPromise.state }, ()=> { return anotherMyPromise.state !== PromiseState .PENDING ; }) return ; } constructor (executor : (resolve : (arg0: T)=>void , reject : (arg0: any )=>void )=>void ){ const resolve = (arg : T ) => { if (this .state !== PromiseState .PENDING ) return ; if (arg instanceof MyPromise ) { this .setToPromise (arg) } this .value = arg; this .state = PromiseState .FULFILLED ; } const reject = (arg : unknown ) => { if (this .state !== PromiseState .PENDING ) return ; if (arg instanceof MyPromise ) { this .setToPromise (arg) } this .value = arg; this .state = PromiseState .REJECTED ; } try { executor (resolve, reject) } catch (err) { reject (err); } } public then<V>(onfulfilled : (v : T ) => V, onrejected? : (v : unknown ) => unknown ) : MyPromise <unknown > { return new MyPromise ((resolve, reject ) => { waitUntil (()=> { if (this .state === PromiseState .REJECTED ) { if (!onrejected) { reject (this .value ) return } reject (onrejected (this .value )) } else if (this .state === PromiseState .FULFILLED ) { try { const res = onfulfilled (this .value ) resolve (res) } catch (err) { reject (err) } } }, ()=> { return this .state !== PromiseState .PENDING }) }) } public catch (onrejected : (v : unknown ) => unknown ) { return new MyPromise ((resolve, reject ) => { waitUntil (()=> { if (this .state === PromiseState .REJECTED ) { if (!onrejected) { reject (this .value ) return } reject (onrejected (this .value )) } }, ()=> { return this .state !== PromiseState .PENDING }) }) } public static resolve<V>(arg : V) : MyPromise <V> { if (arg instanceof MyPromise ) return arg; return new MyPromise ((resolve, reject ) => {resolve (arg)}) } }
#all,#race all 和 race 是 Promise 提供的两个比较重要的静态方法。前者接受一个 promise 的数组并返回一个 promise 对象,其将等待所有 promise 都执行完毕且全为 resolved 时收集所有结果。race 方法也接受一个 promise 数组并返回一个 promise 对象,其将等待某 promise 最先执行完成并收集其结果,无论是 resolved 还是 reject。
async,await async 和 await 是两个非常方便的操作符。async 将函数转换成返回 promise 的异步函数 ,结果的类型取决于 return 的结果,如果 return 一个非 promise 的值,其将被包装,如果 return 一个 promise,那就是 return 该 promise。抛出异常则返回一个 rejected 的 promise。
await 对 promise 进行操作,其将等待直到 promise 返回结果。await 的返回值为 promise 的结果。await 必须在 async 函数内部使用 。通过 async 和 await,可以轻易将异步操作转换成同步操作流,对编写非常方便。
await 所接受的 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 const fn1 = async ( ) => { console .log ("异步过程 1" ) return new Promise ((resolve, reject )=> { setTimeout (()=> { resolve (123 ) }, 2000 ) }) }const fn2 = async ( ) => { console .log ("异步过程 2" ) return new Promise <number>((resolve, reject )=> { setTimeout (()=> { resolve (456 ) reject ("dsdsdssd" ) }, 1000 ) }) }const mainFn = async ( ) => { const res1 = await fn1 (); const res2 = await fn2 (); console .log (res1, res2, "wow" ) }mainFn ().then ()