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   }
View Code

可以看出,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}`));
View Code

运行结果

  • 实例化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}`));
View Code

运行结果

  • 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);
      }
    );
  }
View Code

上面的代码是对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     );
View Code

上面的代码中Promise实例在3秒后resolve,给它添加了3个then方法(then1,then2,then3),分别是在执行Promise实例初始化的同时(在其之后),之后2秒,之后4秒,

我们来看一下运行结果

  1. Promise实例初始化的时间:Sat Feb 09 2019 21:06:57 GMT+0800 (中国标准时间)
  2. 给Promise实例添加第一个then方法的时间:Sat Feb 09 2019 21:06:57 GMT+0800 (中国标准时间)
  3. 给Promise实例添加第二个then方法的时间:Sat Feb 09 2019 21:06:59 GMT+0800 (中国标准时间)
  4. Promise实例resolved的时间:Sat Feb 09 2019 21:07:00 GMT+0800 (中国标准时间)
  5. 第一个then方法的回调函数执行的时间:Sat Feb 09 2019 21:07:00 GMT+0800 (中国标准时间)
  6. 第一个then方法:Promise实例resolved
  7. 第二个then方法的回调函数执行的时间:Sat Feb 09 2019 21:07:00 GMT+0800 (中国标准时间)
  8. 第二个then方法:Promise实例resolved
  9. 给Promise实例添加第三个then方法的时间:Sat Feb 09 2019 21:07:01 GMT+0800 (中国标准时间)
  10. 第三个then方法的回调函数执行的时间:Sat Feb 09 2019 21:07:01 GMT+0800 (中国标准时间)
  11. 第三个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的状态决定。

  1. 只有p1,p2,p3的状态都是fulfilled时,p的状态才会变成fulfilled,此时p1,p2,p3的返回值组成一个数组,传递给p的(resolve)回调函数
  2. 只要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     )
View Code

八、一些有用的附加方法

    /**
     * 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都会执行的操作。
具体应用有
  1. 放在Promise的then和catch方法的最后,始终捕获异常,并且使用setTimeout抛出异常给全局
  2. 做一下无论成功与否都要执行的操作,比如异步通信,在通信结束(无论成功还是失败)后,关闭服务器。

 九、常见应用

  1.  记录异步操作的状态
  2. 与Generator函数结合使用,具体实现方式会在异步操作的文章中介绍。

 参考: 《ES6 标准入门》《深入理解ES6》

作成:2019-02-09

修改: 

  1. 2019-02-16 18:44:48 修改语法错误
posted on 2019-02-09 22:16  西门本不吹雪  阅读(746)  评论(0编辑  收藏  举报