JavaScript Promise小结
事件模型是在事件发生之前绑定监听事件,在事件发生时监听到事件的发生。
如果事件发生之后进行绑定监听事件,则监听不到事件的发生。
ES6提供的Promise实现了单个值的任意时刻的监听,Rxjs的Observable提供了对一系列值的任意时刻的监听。
所谓的任意时刻,指的是在值发生改变之后进行监听也能监听到变化。
本质上promise和observable都是单例的,每一次的then或者observable都会得到新的实例。
创建Promiose时,回调会立即执行,待有then方法时,then方法的回调会进入微队列,等待promise中返回resolve或者reject。
另外,promise是绝对异步的,即使Promise中立即resolve,then的回调还是会进入微队列中,等待同步事件执行完才执行回调。
但是,创建Observable的回调不会立即执行,要等待有subscribe时才会执行,而且,subscribe的回调也不会立即进入队列中,相当于把observer的回调传到创建Observable的代码中,如果创建代码中有异步的,next就会是异步的。
记住:
创建Promise的代码会立即执行,有then是先把回调放入队列,不管这时是否已经resolve或者reject
创建Observable的代码不会立即执行(像是function),只有subscribe是才会执行(调用function)
一、何为Promise
所谓promise,就是一个对象,用来传递异步操作的信息。它代表了某个未来才会知道结果的事件(通常是一个异步操作)。
Promise有自己的生命周期,在定义时,它是unsettled(未处理的),一旦异步操作执行结束,Promise就会变成settled(已处理的)。
Promise还有3个状态,保存在内部属性[[PromiseState]]中,分别为‘pending’,‘fulfilled’,‘rejected’。内部属性不能被外部改变,只能自身改变。
当Promise是unsettled时,它一定处于pending状态,当Promise是settled时,它一定是fulfilled或者rejected状态。
Promise最大的问题是,当处于pending状态时,无法得知程序进行到哪一个阶段,是刚刚开始还是快要结束。
二、创建基本的Promise
ES6规定,Promise对象是一个构造函数,可以原来生成Promise实例。
/** * Creates a new Promise. * @param executor A callback used to initialize the promise. This callback is passed two arguments: * a resolve callback used to resolve the promise with a value or the result of another promise, * and a reject callback used to reject the promise with a provided reason or error. */ new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
可以看到。构造函数接受一个执行器函数,这个执行器函数有2个参数。
一个叫作resolve,调用这个resolve方法可以把Promise从pending状态变成fulfilled状态。
一个叫作reject,调用这个reject方法可以把Promise从pending状态变成rejected状态。
1 new Promise( 2 (resolve, reject) => { 3 const xhr = new XMLHttpRequest(); 4 xhr.open('get', url, true); 5 xhr.responseType = 'json'; 6 xhr.setRequestHeader('Accept', 'application/json'); 7 xhr.onload = function(ev: ProgressEvent) { 8 resolve(xhr.response);// 把Promise的状态从pending变成fulfilled 9 }; 10 xhr.onerror = function(ev: ProgressEvent) { 11 reject('发生了错误');// 把Promise的状态从pending变成rejected 12 }; 13 xhr.send(null); 14 } 15 );
需要说明的是,Promise在实例化的时候就会执行执行器中的逻辑(立即执行,then方法的回调进入队列中)
1 constructor() { 2 3 const user = this.ajax('http://localhost:3002/users'); 4 } 5 6 ajax(url) { 7 return new Promise( 8 (resolve, reject) => { 9 const xhr = new XMLHttpRequest(); 10 xhr.open('get', url, true); 11 xhr.responseType = 'json'; 12 xhr.setRequestHeader('Accept', 'application/json'); 13 xhr.onload = function(ev: ProgressEvent) { 14 console.log('success'); 15 resolve(xhr.response); 16 }; 17 xhr.onerror = function(ev: ProgressEvent) { 18 console.log('error'); 19 reject('发生了错误'); 20 }; 21 xhr.send(null); 22 } 23 ); 24 }
可以看出,Promise本身和异步操作没有关系,Promise只是保存状态(状态带有数据),外部不能获取这种状态,但是可以定义处于某种状态时要触发的操作(使用then和catch)。
还有一点需要注意,resolve函数的参数可以是普通的参数,也可以是一个PromiseLike的对象。
所谓的PromiseLike,就是有在该对象上有then方法,当然,Promoise本身就是PromiseLike,这里只讨论resolve的参数是Promise的情况。
resolve携带参数可以理解为Promise在状态上附加的数据,resolve(value)理解为Promise状态为fulfilled,并且这个状态有数据value。
但是如果是resolve(一个新的Promise),则理解不同,可以理解为当前Promise能不能变成fulfilled状态依赖这个新的Promise的状态,
- 如果这个新的Promise的状态是pending,那么当前的Promise的状态将维持pending,
- 当这个新的Promise的状态由pending变成了fulfilled,那么当前的Promise的状态由pending变成fulfilled,
- 当这个新的Promise的状态由pending变成了rejected,那么当前的Promise的状态由pending变成rejected,
1 const p1 = new Promise( 2 (resolve, reject) => { 3 console.log(`实例化p1的时间:${new Date()}`); 4 setTimeout( 5 () => { 6 console.log(`3秒后reject P1:${new Date()}`); 7 reject('p1发生了Error'); 8 }, 3000 9 ); 10 } 11 ); 12 const p2 = new Promise( 13 (resolve, reject) => { 14 console.log(`实例化p2的时间:${new Date()}`); 15 setTimeout( 16 () => { 17 console.log(`1秒后resolve P2:${new Date()}`); 18 resolve(p1); 19 }, 1000 20 ); 21 } 22 ); 23 p2.then(r => console.log(`p2的状态变成fulfilled的时间:${new Date()}, 接受到的信息: ${r}`)) 24 .catch(r => console.log(`p2的状态变成reject的时间:${new Date()}, error的信息: ${r}`));
运行结果
- 实例化p1的时间:Sat Feb 09 2019 18:13:04 GMT+0800 (中国标准时间)
- 实例化p2的时间:Sat Feb 09 2019 18:13:04 GMT+0800 (中国标准时间)
- 1秒后resolve P2:Sat Feb 09 2019 18:13:05 GMT+0800 (中国标准时间)
- 3秒后reject P1:Sat Feb 09 2019 18:13:07 GMT+0800 (中国标准时间)
- p2的状态变成reject的时间:Sat Feb 09 2019 18:13:07 GMT+0800 (中国标准时间), error的信息: p1发生了Error
可以看到
- P1的实例化和在P2中是否使用到P1完全没有关系,
- 1秒后本来要resolve P2的,但是由于P2的resolve函数的参数是一个Promise(P1),此时P2会一直处于pending状态
- 直到2秒后,P1的状态发生改变,由pending变成了reject,这时,原本要resolve的P2因为受到P1的影响,不能变成fulfilled状态了,变成了rejected状态,所以才会被catch
- 被catch的时间就是P1状态改变的时间,也是P2状态改变的时间
实际的业务中,P1和P2的实例化相互不影响的情况比较少见,多数情况是P1的实例化依赖于p2。比如先通信取得人名,再通过人名得到该人名对应的家庭成员。
P2中通过ajax取得人名,在取得成功(resolve)之后,通过人名取得家庭成员(p1)中
1 const p2 = new Promise( 2 (resolve, reject) => { 3 console.log(`p2的实例化的时间:${new Date()}`) 4 const xhr = new XMLHttpRequest(); 5 xhr.open('get', 'http://localhost:3002/users?id=1', true); 6 xhr.responseType = 'json'; 7 xhr.setRequestHeader('Accept', 'application/json'); 8 9 xhr.addEventListener('load', function(ev: ProgressEvent) { // p2通过id成功取得name 10 console.log(`p2的resolve的时间:${new Date()}`) 11 resolve(new Promise( 12 (res, rej) => { 13 console.log(`p1的实例化的时间:${new Date()}`) 14 const xhr1 = new XMLHttpRequest(); 15 // 使用P2取得的name搜索取得该name对应的家庭成员 16 xhr1.open('get', `http://localhost:3002/family?name=${xhr.response[0].name}`, true); 17 xhr1.responseType = 'json'; 18 xhr1.setRequestHeader('Accept', 'application/json'); 19 xhr1.onload = function() { 20 // 把家庭成员发送出去 21 console.log(`p1的resolve的时间:${new Date()}`) 22 resolve(xhr1.response[0].families) 23 } 24 xhr1.send(null); 25 } 26 )); 27 }); 28 xhr.send(null); 29 } 30 ); 31 p2.then(r => console.log(`p2的状态变成fulfilled的时间:${new Date()}, 接受到的信息: ${r}`)) 32 .catch(r => console.log(`p2的状态变成reject的时间:${new Date()}, error的信息: ${r}`));
运行结果
- p2的实例化的时间:Sat Feb 09 2019 18:49:13 GMT+0800 (中国标准时间)
- p2的resolve的时间:Sat Feb 09 2019 18:49:13 GMT+0800 (中国标准时间)
- p1的实例化的时间:Sat Feb 09 2019 18:49:13 GMT+0800 (中国标准时间)
- p1的resolve的时间:Sat Feb 09 2019 18:49:13 GMT+0800 (中国标准时间)
- p2的状态变成fulfilled的时间:Sat Feb 09 2019 18:49:13 GMT+0800 (中国标准时间), 接受到的信息: 独孤求败,梅超风
可以看到
- 整体的代码逻辑和同步的代码没有什么区别
- 执行顺序和同步的代码也是相同的
- p1的状态附带数据能够传递给P2,然后再传递给resolve方法
二 、创建立即fulfilled的Promise
使用原始的Promise构造函数
1 const p = new Promise( 2 (resolve, reject) => { 3 resolve('foo'); 4 } 5 );
ES6对其做了简化,可以使用Promise.resolve('foo')
三、创建立即reject的Promise
使用原始的Promise构造函数
1 const p = new Promise( 2 (resolve, reject) => { 3 reject('error'); 4 } 5 );
ES6对其做了简化,可以使用Promise.reject('foo')
四、Promise实例的then方法
4.1 then方法的说明
Promise实例具有then方法。也就是说,then方法是定义在原型对象Promise.prorotype上的。它的作用是为Promise实例添加状态改变时的回调函数。
/** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of which ever callback is executed. */ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
可以看到,then方法有2个参数,
- then方法返回的还是Promise,所以可以链式调用。
- 第一个参数是onfulfilled,是Promise实例从pending状态变成fulfilled状态时的回调函数。
- 第二个参数是onrejected,是Promise实例从pending状态变成rejected状态时的回调函数。
- onfulfilled和onrejected都可以为undefined和null,也就是说,可以进行任意组合,只指定onfulfilled或者只指定onrejected或者全都指定。
- 注意到onfulfilled的参数是一个普通的值(不是一个promise),也就是说,就算Promise实例中resolve的是一个新的Promise,下游then方法接收到的值还是新的Promise发出的值,不是这个新的Promise。
- onfulfilled和onrejected的返回值都支持PromiseLike,和之前在Promise实例中调用resolve方法时的参数是一样的,我理解其实在then方法中return PromiseLike和在Promise实例中resolve PromiseLike是一样的。
ajax(url) { return new Promise( function (resolve, reject) { const xhr = new XMLHttpRequest(); xhr.open('get', url, true); xhr.responseType = 'json'; xhr.setRequestHeader('Accept', 'application/json'); xhr.addEventListener('load', function(ev: ProgressEvent) { resolve(xhr.response); }); xhr.addEventListener('error', function(ev: ProgressEvent) { reject('发生了错误'); }); xhr.send(null); } ); }
上面的代码是对ajax的封装,
1 const ajax = this.ajax('http://localhost:3002/users?id=1'); 2 ajax.then( 3 (response) => { 4 return this.ajax(`http://localhost:3002/family?name=${response[0].name}`); 5 } 6 ).then( 7 (response) => { 8 console.log(response) 9 } 10 )
上面的代码中在then方法中return 了一个新的Promise,其实这个例子和之前说明Promise的resolve方法可以把Promise实例作为参数是一样的。
4.2 then方法添加和Promise处于什么状态没有关系
我们知道,Promise有3个状态,分别是pending,fulfilled和rejected状态。
状态由pending变成fulfilled实际上说明Promise实例附带的操作已经完成了。对于事件监听,在事件完成之后添加回调函数是监听不到事件的,但是Promise这种方式在操作完成之后也可以监听到。
实际上,操作只是会触发Promise实例的状态的改变,而Promise实例的then方法是监听状态的,不是监听操作的,可以理解为
- 给Promise实例添加then方法时,JS会去查看Promise的内部属性[[promiseState]],当[[promiseState]]是pending时,则等待Promise的状态改变(这里类似添加事件监听),一旦改变则执行回调函数
- 如果在添加then方法时,[[promiseState]]就已经是fulfilled或者reject时,则立即执行回调函数。(但是,这里的立即执行也是在回调函数已经进入微队列的情况下的)
- 只要Promise实例的状态发生了改变,在之后的任意时刻添加then方法得到的都是同一个结果。
1 const promise = new Promise( 2 (resolve, reject) => { 3 console.log(`Promise实例初始化的时间:${new Date()}`); 4 setTimeout( 5 () => { 6 console.log(`Promise实例resolved的时间:${new Date()}`); 7 resolve('Promise实例resolved') 8 }, 9 3000 10 ); 11 } 12 ); 13 console.log(`给Promise实例添加第一个then方法的时间:${new Date()}`); 14 promise.then( 15 (v) => { 16 console.log(`第一个then方法的回调函数执行的时间:${new Date()}`); 17 console.log(`第一个then方法:${v}`); 18 } 19 ); 20 setTimeout( 21 () => { 22 console.log(`给Promise实例添加第二个then方法的时间:${new Date()}`); 23 promise.then( 24 (v) => { 25 console.log(`第二个then方法的回调函数执行的时间:${new Date()}`); 26 console.log(`第二个then方法:${v}`); 27 } 28 ); 29 }, 30 2000 31 ); 32 setTimeout( 33 () => { 34 console.log(`给Promise实例添加第三个then方法的时间:${new Date()}`); 35 promise.then( 36 (v) => { 37 console.log(`第三个then方法的回调函数执行的时间:${new Date()}`); 38 console.log(`第三个then方法:${v}`); 39 } 40 ); 41 }, 42 4000 43 );
上面的代码中Promise实例在3秒后resolve,给它添加了3个then方法(then1,then2,then3),分别是在执行Promise实例初始化的同时(在其之后),之后2秒,之后4秒,
我们来看一下运行结果
- Promise实例初始化的时间:Sat Feb 09 2019 21:06:57 GMT+0800 (中国标准时间)
- 给Promise实例添加第一个then方法的时间:Sat Feb 09 2019 21:06:57 GMT+0800 (中国标准时间)
- 给Promise实例添加第二个then方法的时间:Sat Feb 09 2019 21:06:59 GMT+0800 (中国标准时间)
- Promise实例resolved的时间:Sat Feb 09 2019 21:07:00 GMT+0800 (中国标准时间)
- 第一个then方法的回调函数执行的时间:Sat Feb 09 2019 21:07:00 GMT+0800 (中国标准时间)
- 第一个then方法:Promise实例resolved
- 第二个then方法的回调函数执行的时间:Sat Feb 09 2019 21:07:00 GMT+0800 (中国标准时间)
- 第二个then方法:Promise实例resolved
- 给Promise实例添加第三个then方法的时间:Sat Feb 09 2019 21:07:01 GMT+0800 (中国标准时间)
- 第三个then方法的回调函数执行的时间:Sat Feb 09 2019 21:07:01 GMT+0800 (中国标准时间)
- 第三个then方法:Promise实例resolved
可以看到
- (2,3)then1和then2添加是,Promise实例还没有resolve,此时Promise实例应该处于pending状态,添加then1和then2后,回调函数会被放在任务队列中,等待状态的改变
- (4)Promise实例此时已经resolve,状态变成fulfilled,这个时候之前添加的then1和then2的回调函数会被执行(5,6,7,8)
- (9)在Promise实例resolved后添加then方法,回调函数会被立即执行(10,11),当然,回调函数即使是立即执行,也是被放到下一个队列中(所谓的同步再慢也比异步快)
五、Promise实例的catch方法
/** * Attaches a callback for only the rejection of the Promise. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of the callback. */ catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
Promise.prototype.catch方法是then(null, onrejected)的简化形式。
Promise的错误具有冒泡性质,会一直向后面传递,直到被捕获为止。
如果没有给Promise对象指定异常捕获的方法(then方法的onrejected或者catch方法),Promise对象抛出的错误不会传递到外层代码,不会有任何反应。
catch方法也可以返回PromiseLike对象,与then方法类似。
六、Promise对象的all方法
Promise.all方法原来把多个Promise实例包装成一个Promise实例。
let p = Promise.all([p1, p2, p3]);
/** * Creates a Promise that is resolved with an array of results when all of the provided Promises * resolve, or rejected when any Promise is rejected. * @param values An array of Promises. * @returns A new Promise. */ all<T1, T2, T3>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;
可以看到,ts中的Promise.all方法的参数是一个数组,数组中的元素可以是普通值,也可以是一个PromiseLike对象,当是普通·值时,会被自动的调用Promise.resolve()方法转换我Promise
p的状态由p1,p2,p3的状态决定。
- 只有p1,p2,p3的状态都是fulfilled时,p的状态才会变成fulfilled,此时p1,p2,p3的返回值组成一个数组,传递给p的(resolve)回调函数
- 只要p1,p2,p3中有一个被rejected,p的状态就会变成reject,此时第一个被reject的实例的返回值会传递给p的(reject)回调函数。
实际应用中等待多个异步通信都完成(处理并发)是会用到Promise.all
七、Promise对象的race方法
/** * Creates a Promise that is resolved or rejected when any of the provided Promises are resolved * or rejected. * @param values An array of Promises. * @returns A new Promise. */ race<T1, T2, T3>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<T1 | T2 | T3>;
Promise.race方法同样是将多个Promise实例包装成一个新的Promise实例。
let p = Promise.race([p1, p2, p3]);
上面的代码中,只要p1,p2,p3中有一个实例的状态首先发生改变,p的状态就会跟着改变,首先发生改变的实例的返回值,就会被传递给p的回调函数。
下面的例子是设置通信的超时。
1 Promise.race([ 2 this.ajax('http://localhost:3002/users?id=1'), 3 new Promise( 4 (resolve, reject) => { 5 setTimeout( 6 () => reject('超过3000ms了'), 7 3000 8 ); 9 } 10 ) 11 ]).then( 12 (v) => console.log(v) 13 ).catch( 14 (err) => console.log(err) 15 )
八、一些有用的附加方法
/** * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The * resolved value cannot be modified from the callback. * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). * @returns A Promise for the completion of the callback. */ finally(onfinally?: (() => void) | undefined | null): Promise<T>
finally方法支持2类参数,一个回调函数或者null,
finally可以理解为无论Promise是resolve还是reject都会执行的操作。
具体应用有
- 放在Promise的then和catch方法的最后,始终捕获异常,并且使用setTimeout抛出异常给全局
- 做一下无论成功与否都要执行的操作,比如异步通信,在通信结束(无论成功还是失败)后,关闭服务器。
九、常见应用
- 记录异步操作的状态
- 与Generator函数结合使用,具体实现方式会在异步操作的文章中介绍。
参考: 《ES6 标准入门》《深入理解ES6》
作成:2019-02-09
修改:
- 2019-02-16 18:44:48 修改语法错误