ES6+ Promise 基础
1. 前言
我们知道浏览器在渲染网页时,会创建 一个 渲染进程进行渲染 页面 ,在渲染进程中其中有 GUI 渲染线程和 JS 引擎线程(如 V8 引擎)两个线程是互斥的。也就是说在同一时间内只能有 一个 线程执行。如果 JavaScript 执行一段耗时程序时会阻止 页面 渲染。如果要 页面 快速 在 用户 面前呈现就 要做 一些优化处理。对于不能立马得到结果的程序,不需要等待,可以放到事件队列中,等到得到结果后再执行。
对这种不等待方式,JavaScript 提供了异步的 解决方 案,在 JavaScript 中常见的异步 解决方 案是 Callback 方式,而像 setTimeout 这样提供异步的 API,还可以使用发布 订阅 的来实现异步。使用回调 函数 存在回调地狱的问题。为了 解决 回调地狱,最早社区提出了 Promise 概念。最后在 ES6 时正式作为官方的 解决方 案,说明 Promise 有它独有的优势。本节我们将学习 Promise 的基本 用法 。
2. 回调地狱
我们都知道 JavaScript 异步使用的是回调 函数 ,下面我们来看 一个 ajax 请求的实例,下面的 ajax 方法 是 一个 伪 代码 ,可以看作是请求接口的 方法 ,接口请求的库可以参考 jQuery 的 $.ajax 和 axios。
// ajax请求的伪 代码 function ajax ( url , sucessCallback , failCallback ) { // url:请求的url // sucessCallback:成功的回调 函数 // failCallback:失败的回调 函数 } ajax ( url1 , ( res1 ) => { ajax ( url2 , ( res2 ) => { ajax ( url3 , ( res3 ) => { doSomething ( res1 , res2 , res3 ) } ) } ) } )
上面的 ajax 请求我们可以理解为,在 调用 doSomething 方法 时需要前面三个请求的结果作为参数,所以只有前 一个 ajax 请求得到结果后才能发起第二个请求。这样前后有依赖的嵌套被称为回调地狱。对于比较复杂逻辑的情况来说,回调地狱会使程序出问题的概率大大 增加 。
另外,这样做有个很严重的问题,就是接口请求的时间是三个请求的和,不能进行并发操作,当然我们也可以做一些优化操作,如下:
let out = after ( , function ( data ) { doSomething ( ... data ) } ) ajax ( url1 , ( res1 ) => { out ( res1 ) } ) ajax ( url2 , ( res2 ) => { out ( res2 ) } ) ajax ( url3 , ( res3 ) => { out ( res3 ) } ) function after ( times , callback ) { const arr = [ ] ; return function ( value ) { arr . push ( value ) ; if ( -- times == ) { callback ( arr ) ; } } }
上面的 代码 很优雅地 解决 了回调嵌套的问题,但同时我们需要手动维护 一个 计数器来控制最后的回调。这无疑 增加 了程序的复杂度,我们更希望的是关注我的业务,而不是写更多的逻辑来优化。
针对这种情况,社区提供了很多这类优化的库,而 Promise 则是其中最亮眼的。对上面的情况,Promise 怎么 解决 的呢?看如下的实现方式:
function request ( url ) { return new Promise ( ( resolve , reject ) => { ajax ( url , ( res ) => { resolve ( res ) } ) } ) } Promise . all ( [ request ( url1 ) , request ( url1 ) , request ( url1 ) ] ) . then ( ( result ) => { doSomething ( ... result ) } ) . catch ( ( error ) => { console . log ( error ) } )
上面的 代码 中我们封装了 一个 request 请求的 方法 ,通过 Promise.all() 来并发请求这些接口,当接口都正确返回才会执行 then 方法 中的回调,有 一个 错误 都会抛出异常。这种方式比较好的是,我们对请求进行了封装,不要再关注每一步请求是否完成做对应的逻辑处理,让我们在开发过程中更加关注业务逻辑,使开发效率更快。
3. Promise 用法
前面我们通过 一个 回调地狱的案例,说明了 Promise 的优点,就是为了 解决 异步而产生的。并且可以处理并发请求,很好地优化了程序资源。
3.1 实例化 一个 Promise
首先需要明确 Promise 是 一个 类,我们在 VSCode 中输入 new Promise() 会给我们如下的 提示 :
在 new Promise() 时需要 默 认需要传入 一个 回调 函数 ,这个回调 函数 是 executor(执行器), 默 认会立即执行。执行器会提供两个 方法 (resolve 和 reject)用于改变 promise 的状态。 resolve 会触发成功状态, reject 会触发失败状态,无论成功或失败都会传入 一个 返回值,这个返回值会在实例 调用 then 方法 后作为响应值 获取 。
var promise = new Promise ( ( resolve , reject ) => { ajax ( url , ( data ) => { resolve ( data ) // 成功 } , ( error ) => { reject ( error ) // 失败 } ) } )
上面的 代码 中实例化 一个 ajax 请求的 Promise, 当接口请求成功就会 调用 resolve () 方法 把请求的值传入进去,如果失败了就 调用 reject () 方法 把 错误 信息传入进去。在后续的链式 调用 中 获取 相应的结果。
我们需要知道的是,Promise 有三个状态:等待(padding)、成功(fulfilled),失败(rejected)。在初始化时,这个状态是等待态,在等待状态时可以转化为成功态或失败态。当状态是成功态或是失败态后不能再被改变了。
上面的 代码 中可以改变 Promise 状态的是执行器提供的 resolve 和 reject,resolve 会将等待态变为成功态,reject 则会将等待态变为失败态,在状态变为成功或失败的状态时就不能被更改了。
3.2 then
在实例化 Promise 类后,我们如何访问成功和失败的值呢?Promise 提供了链式 调用 的 then 方法 用于访问成功的值和失败的值,then 提供了两个回调 onfulfilled(成功回调)、onrejected(失败回调)。
var promise = new Promise ( ( resolve , reject ) => { resolve ( ) ; // reject('error') // throw new Error('Error') } ) promise . then ( ( data ) => { console . log ( data ) // 123 return '100' } , ( reason ) => { console . log ( reason ) } ) . then ( ( data ) => { console . log ( data ) // 100 } , null )
上面的 代码 中给我了几个测试用例,有兴趣的小伙伴可以进行测试。then 方法 返回 一个 值而不是 Promise 实例,并且会把这个结果返回到下 一个 then 的成功回调中;
如果返回的是 一个 promise,下 一个 then 会采用这个 promise 结果,如果返回的是失败,会传到下 一个 then 中失败的回调 函数 中去:
var promise = new Promise ( ( resolve , reject ) => { resolve ( ) ; } ) promise . then ( ( data ) => { return new Promise ( ( resolve , reject ) => { reject ( ' 错误 内容 ' ) ; } ) } , null ) . then ( null , ( err ) => { console . log ( 'error:' , err ) // error: 错误 内容 } )
如果在失败的回调 函数 中返回 一个 普通值或成功的 promise 也会走到下一层 then 的成功回调中去。
promise . then ( null , ( err ) => { return '100' ; } ) . then ( ( data ) => { console . log ( 'data:' , data ) ; // data: 123 } , null )
通过上面的例子可以知道,当前 then 中走成功与否,主要看上一层返回的结果。总结有两点。
当上一层返回 一个 普通值,或是 一个 成功的 Promise,则会走到下一层成功的回调 函数 中去; 如果上一层返回 一个 失败的 Promise,或是抛出 一个 异常,则会走到下一层的失败的回调 函数 中去。
4. 小结
本节主要通过 JavaScript 中回调地狱的 一个 案例来引出为什么使用 Promise,以及 Promise 所带来的好处。然后学习了 Promise 的基本使用和链式 调用 then 方法 ,需要注意的是,then 中执行成功或是失败是根据它上一层的返回值,如果返回的是 一个 普通值或成功的 Promise 则会走 then 的成功回调;如果抛出异常或返回失败的 Promise 则走 then 的失败回调。