Promise理解

唠唠叨叨

最近考试比较多,终于快结束了,间隙时间,我第一遍看了《你不知道的JavaScript》的上中篇,说实话,可能是因为译本的原因,也可能是知识比较深入,我感觉比较难理解,有时感觉一小节讲完,发现他的中心观点只有一句话。但总的来说,这是套值得深入研读的书籍,暑假我将再次研读。

Promise的神秘面纱

ES6的知识点着实比较庞大,除了对Number,Array等的扩展。Promise这个概念也很吸引人。比如著名的axios就是采用Promise来封装的。

我在《你不知道的JavaScript》中看到一个比较形象的比喻,当你去买一个汉堡包,你先把钱给服务员,然后服务员会给你一个凭证,在这个汉堡包被完成之前,凭证即代表了你的汉堡包。一段时间过后,服务员通知你结果。但这是服务员可能会对你说:‘Sorry Sir,hamburger has sold out…’这种失败的结果即为Promise中的reject。但大多数情况下,我们期望并得到的结果是一个汉堡包 ( solved)。

也就是说:Promise是一个容器,里面保存着未来才会结束的事件(通常是一个一步操作)的结果。

Promise的特点

  1. Promise的状态不收外界的影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义promise
var promise = new Promise(function(resolve,reject) {
var res = asycFunc();
if(res.code 满足条件){
resolved(res.data);
}else{
reject(res.data)
}
})

//调用promise

//1(不推荐).
promise.then(res => {show(res)},err=>(handleError(err)))
//2(推荐).
promise.then(res => {show(res)}).catch(err => {handleError(err)})

第二种的好处在于,1.采用链式的写法可以捕获前一个then抛出的错误,而一then中同时写resolve与reject无法做到。2.在语法上更人性化,类似于try…catch…语法。但实际catch = then(null,rejection).
都是捕获产生的错误,包括rejected,Error;对于错误,还有一点值得注意的是,reject或者resolve即代表这个promise状态的冻结。类似于函数的return,这两个语句一旦发生,其后面所有的语句都将无效。即使是抛出错误,也无法再被catch所捕获。

Promise的链式玩法

前面的then…catch…其实已经是链式的写法了。

Promise有3个函数可以链式调用。分别是

  1. then(func(reslved,rejected){...}): 为Promise实例添加状态改变时的回调函数。
  2. catch(func(err){...}): 用于指定发生错误时的回调函数。包括rejected也被他处理。
  3. finally(func(){...}): 无论状态如何,是否抛出错误。都将执行该回调。

但其能够一直链式调用的原因是:这3个函数每次调用都会产生一个新的Promise对象,并且这个新的promise会将旧的promise返回值作为参数,这个对象又包含了这3个方法,所以可以一直链式调用。

Promise的高级玩法(模式)

  1. Promise.all([p1,p2,…])

说明:该方法用于将多个Promise实例包装成一个Promise实例。返回值判定方法为:

a. 所有的Promise的值变为resolved时,该对象才返回resolved,并返回所有的结果。
b. 一旦有一个Promise对象返回rejected,则整个对象返回rejected,并返回错误原因。

由Promise封装的axios中,有axios.all([re1,re2,…])方法

  1. Promise.race([p1,p2,…])

说明:该方法用于将多个Promise实例包装成一个Promise实例。返回值判定方法为:

a. 当第一个Promise返回resolved时,整个Promise状态变为resolved,并返回第一个Promise resolved的结果,一旦有任何一个Promise决议为拒绝,他就会拒绝。

值得注意的

  1. Promise.all在传入空数组时,会立即返回resolved;但Promise.race会被一直挂起。所以不要想Promise.race中传递空数组。希望ECMA会尽快修改这一点。

一些骚操作

在《你不知道JavaScript》一书中,提出来一些ES6标准之外的Promise模式之外的一些扩展模式,比如:

  1. Promise.none([...]):与Promise.all的情况互换,即所有的Promise都rejected才rejected。反之则返回resolved。
  2. Promise.any([...]):与race相对,该模式忽略rejected,得到第一个resolved。
  3. Promise.first([...]):这个模式类似与any([…])的竞争,,即只要第一个Promise完成,他就会忽略有序的任何拒绝和完成。
  4. Promise.last([...]):这个类似与first([…],但确实最后一个完成胜出。

示例:Promise.first([...])的实现

//polyfill安全的guard检查
if(!Promise.first){
    Promise.first = function(prs){
        //返回最终的Promise对象
        return new Promise(function(resolve,reject){
            //把所有的Promise循环一次
            prs.forEach(function(pr){
                //把值规整化,并且不管哪个先完成,就决议组Promise
                Promise.resolve(pr).then(resolve);
            })
        })
    }
}

Promise的实际应用

  1. 异步加载图片
1
2
3
4
5
6
7
8
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
  1. 异步Ajax

    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
    const getJSON = function(url) {
    const promise = new Promise(function(resolve, reject){
    const handler = function() {
    if (this.readyState !== 4) {
    return;
    }
    if (this.status === 200) {
    resolve(this.response);
    } else {
    reject(new Error(this.statusText));
    }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

    });

    return promise;
    };

    getJSON("/posts.json").then(function(json) {
    console.log('Contents: ' + json);
    }, function(error) {
    console.error('出错了', error);
    });

最后

Promise着实避免了回调函数的一些缺陷,并且它的使用也越来越广泛,包括后面的async异步函数,也是利用Promise来完成的,因此,这里简单的了解一下Promise还是很有必要的。

2021年1月18日补充

概念

  1. 其在新建时就开始立即执行,一旦完成,其状态就会”凝固“。

  2. “凝固”过后其状态就不会发生改变,并且与事件不同的是,其可以随之被监听,即:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const p1 = new Promise(resolve, reject){
    setTimeout(()=>{resolve(1)}, 3000)
    }

    p1.then((res) => {
    console.log(`${res}--第一次获取`)
    })

    p1.then((res) => {
    console.log(`${res}--第二次获取`)
    })

    第一个结果为:1—第一次获取

    第二个结果为:1—第二次获取

    注意:这两次执行都会在3秒过后执行,并且都会执行;这里区别于普通事件,一旦错过,就无法再监听到了。但是Promise在操作完成后会凝固状态,无论后面在什么时候进行监听(定义其then函数)都会被执行。

以实现的接口

  1. Promise.all([p1, p2, p3]),其返回结果分为两种:

    1. 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

    2. 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

      注意:注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。如果p2没有自己的catch方法,就会调用Promise.all()catch方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      const p1 = new Promise((resolve, reject) => {
      resolve('hello');
      })
      .then(result => result)
      .catch(e => e);

      const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了');
      })
      .then(result => result)
      .catch(e => e);

      Promise.all([p1, p2])
      .then(result => console.log(result))
      .catch(e => console.log(e));
      // ["hello", Error: 报错了]
  2. Promise.race([p1, p2, p3]):上面代码中,只要p1p2p3之中有一个实例率先改变状态(无论是fulfilled还是reject),p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

    下面这个即利用这个特性完成请求超时:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const p = Promise.race([
    fetch('/resource-that-may-take-a-while'),
    new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
    })
    ]);

    p
    .then(console.log)
    .catch(console.error);
    1. Promise.allSettled(p1, p2, p3):方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

      该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const resolved = Promise.resolve(42);
    const rejected = Promise.reject(-1);

    const allSettledPromise = Promise.allSettled([resolved, rejected]);

    allSettledPromise.then(function (results) {
    console.log(results);
    });
    // [
    // { status: 'fulfilled', value: 42 },
    // { status: 'rejected', reason: -1 }
    // ]
  3. Promise.any(p1, p2, p3) :该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。(与all相反)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const promises = [
    fetch('/endpoint-a').then(() => 'a'),
    fetch('/endpoint-b').then(() => 'b'),
    fetch('/endpoint-c').then(() => 'c'),
    ];
    try {
    const first = await Promise.any(promises);
    console.log(first);
    } catch (error) {
    console.log(error);
    }

    Promise.any()抛出的错误,不是一个一般的错误,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误。下面是 AggregateError 的实现示例。

    捕捉错误时,如果不用try...catch结构和 await 命令,可以像下面这样写。

    1
    2
    3
    4
    5
    6
    7
    8
    Promise.any(promises).then(
    (first) => {
    // Any of the promises was fulfilled.
    },
    (error) => {
    // All of the promises were rejected.
    }
    );

    下面是一个例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var resolved = Promise.resolve(42);
    var rejected = Promise.reject(-1);
    var alsoRejected = Promise.reject(Infinity);

    Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
    console.log(result); // 42
    });

    Promise.any([rejected, alsoRejected]).catch(function (results) {
    console.log(results); // [-1, Infinity]
    });
    1. Promise.resolve():有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

      1. 参数是一个 Promise 实例

        如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

      2. 参数是一个thenable对象

        thenable对象指的是具有then方法的对象,比如下面这个对象。

        1
        2
        3
        4
        5
        let thenable = {
        then: function(resolve, reject) {
        resolve(42);
        }
        };

        Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        let thenable = {
        then: function(resolve, reject) {
        resolve(42);
        }
        };

        let p1 = Promise.resolve(thenable);
        p1.then(function (value) {
        console.log(value); // 42
        });

        上面代码中,thenable对象的then()方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then()方法指定的回调函数,输出42。

      3. 参数不是具有then()方法的对象,或根本就不是对象

        如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved

        1
        2
        3
        4
        5
        6
        const p = Promise.resolve('Hello');

        p.then(function (s) {
        console.log(s)
        });
        // Hello

        上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve()方法的参数,会同时传给回调函数。

      4. Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

  4. Promise.reject():会返回一个新的 Promise 实例,该实例的状态为rejected

    1
    2
    3
    4
    5
    6
    7
    8
    const p = Promise.reject('出错了');
    // 等同于
    const p = new Promise((resolve, reject) => reject('出错了'))

    p.then(null, function (s) {
    console.log(s)
    });
    // 出错了

    Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。

    1
    2
    3
    4
    5
    Promise.reject('出错了')
    .catch(e => {
    console.log(e === '出错了')
    })
    // true

    上面代码中,Promise.reject()方法的参数是一个字符串,后面catch()方法的参数e就是这个字符串。

目前Chrome(87)中的以实现上述方法。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :