0%

深入Promise

  • 缓解回调地狱
  • 让两个异步操作并行,在都结束后提醒你
  • 让两个异步操作并行,只关注首个结束结果

生命周期

Promise有三个状态(内部的[[PromiseState]] 属性):

  1. pending 挂起
  2. fulfilled 成功
  3. rejected 失败
    then()方法在所有Promise上都存在并且接受两个参数。第一个参数是Promise被完成时要调用的函数,与异步操作关联的任何附加数据都会被传入这个完成函数。第二个参数则是Promise被拒绝时要调用的函数,与完成函数相似,拒绝函数会被传入与拒绝相关联的 任何附加数据。
    用这种方式实现then()方法的任何对象都被称为一个 thenable 。所有的Promise都是thenable,反之则未必成立。
    每次调用then()catch()都会创建一个 新的作业,他会在Promise已决议时执行。但这些作业最终会进入一个完全为Promise保留的作业队列。

创建未决的Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise(function(resolve, reject) {
console.log("Promise");
resolve();
});

promise.then(function() {
console.log("Resolved.");
});
console.log("Hi!");

// Promise
// Hi!
// Resolved.

创建已决的Promise

使用Promise.resolve()

Promise.resolve()方法接受单个参数并会返回一个处于完成态的Promise。这意味着没有任何作业调度会发生,并且你需要向Promise添加一个或更多的完成处理函数来提取这个参数值。例如:

1
2
3
4
let promise = Promise.resolve(42);
promise.then(function (value) {
console.log(value); // 42
)

使用Promise.reject()

你也可以使用Promise.reject()。此方法像Promise.resolve()一样工作,区别是被创建的 Promise 处于拒绝态,如下:

1
2
3
4
5
let promise = Promise.reject(42);
promise.catch(function(value) {
console.log(value);
// 42
});

传入Promise

  • 若传入的 Promise 为挂起态
    Promise.resolve()调用不会有返回。此后,若决议原 Promise ,在 then() 中可以接收到原例中的参数42,而若拒绝原 Promise,则在 catch() 中可以接收到参数 42。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    reject(42)
    }, 1000)
    })

    let promise1 = Promise.resolve(promise);
    promise1.then((res) => {
    console.log(res)
    }).catch(e => {
    console.log('error', e)//error 42
    })
    但 Promise.reject()调用则会对原先的 Promise 重新进行包装,对其使用 catch()可以捕捉到错误,处理函数中的value参数不会是数值42,而是原先处于挂起态的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
        let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    reject(42)
    }, 1000)
    })

    let promise1 = Promise.reject(promise)
    promise1.then((res) => {
    console.log(res)
    }).catch(e => {
    console.log('error', e)// error Promise<pending>
    })
    ```
    * 若传入的Promise为完成状态
    `Promise.resolve()`调用会将该Promise原样返回,在 `then()`中可以接收到原例中的参数42
    ```js
    let promise = Promise.resolve(42)

    let promise1 = Promise.resolve(promise)
    promise1.then((res) => {
    console.log(res) // 42
    }).catch(e => {
    console.log('error', e)
    })
    Promise.reject()调用则会对原先的Promise重新进行包装,对其使用catch()可以捕捉到错误,处理函数中的value参数不会是数值42,而是原先处于完成态的Promise。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
        let promise = Promise.resolve(42)
    let promise1 = Promise.reject(promise)
    promise1.then((res) => {
    console.log(res)
    }).catch(e => {
    console.log('error', e) // error Promise {<resolved>: 42}
    })
    ```
    * 若传入的Promise为拒绝态
    `Promise.resolve()`调用会将该Promise原样返回,在`catch()`中可以接收到参数42。
    ```js
    let promise1 = Promise.resolve(promise)
    promise1.then((res) => {
    console.log(res)
    }).catch(e => {
    console.log('error', e)// error 42
    })
    Promise.reject()调用则会对原先的Promise重新进行包装,对其使用catch()可以进行完成处理,处理函数中的value参数不是42,而是原先处于拒绝态的Promise。
    1
    2
    3
    4
    5
    6
    7
    8
    let promise = Promise.reject(42)

    let promise1 = Promise.reject(promise)
    promise1.then((res) => {
    console.log(res)
    }).catch(e => {
    console.log('error', e)//error Promise {<rejected>: 42}
    })

非Promise的Thenable

Promise.resolve()Promise.reject()都能接受非Promise的thenable作为参数。当传入了非Promise的thenable时,这些方法会创建一个新的Promise,此Promise会在then()函数之后被调用。
当一个对象拥有一个能接受resolvereject参数的then()方法,该对象就会被认为是一个非Promise的thenable,就像这样:

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

此例中的thenable对象,除了then()方法之外没有任何与Promise相关的的特征。你可以调用Promise.resolve()来将thenable转换为一个已完成的Promise:

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

let p1 = Promise.resolve(thenable);

console.log(p1) // Promise {<pending>}
p1.then(function (value) {
console.log(value);// 42
});

此例子中,Promise.resolve()调用了thenable.then(),确定了这个thenable的Promise状态:由于resolve(42)thenable.then()方法内部被调用,这个thenable的Promise状态也就被设为已完成。一个名为p1的完成处理函数就接收到一个值为42的参数。


执行器错误

Promise的拒绝处理函数会处理执行器内部抛出的所有错误

全局Promise拒绝处理

如果promise没有catch,抛出来的错误就不会被抛出,node.js和浏览器都提供了相应的事件来监听是否存在没有被监听的错误。

node.js

提供两个事件:

  • unhandledRejection:当一个Promise被拒绝、而在事件循环的一个轮次中没有任何拒绝处理函数被调用,改时间就会被触发。

  • rejectionHandled:若一个Promise被拒绝、并在事件循环的一个轮次之后再有拒绝处理函数被调用,该事件就会被触发。

    1
    2
    3
    4
    5
    6
    7
    8
    let process = require('process');
    let rejected;
    process.on("unhandledRejection", function(reason, promise) {
    console.log(reason.message);// a
    console.log(reason);// 错误堆栈
    console.log(rejected === promise);// true
    });
    rejected = Promise.reject(new Error('a'))

    事件处理函数接受两个参数,第一个参数为错误对象,第二个为原Promise

    rejectionHandled 事件处理函数则只有一个参数,即已被拒绝的Promise。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let process = require('process');
    let rejected;
    process.on("rejectionHandled", function(promise) {
    console.log('res========>',rejected === promise);
    });
    rejected = Promise.reject(new Error('a'))
    setTimeout(() => {
    rejected.catch(e => {
    console.log(e)
    })
    }, 1000)

    此处的rejectionHandled事件在拒绝处理函数最终被调用时触发。若在rejected被创建后直接将拒绝处理函数附加到它上面,那么此事件就不会被触发。因为立即附加的拒绝处理函数在rejected被创建的事件循环的同一个轮次内就会被调用,这样rejectionHandled就不会起作用。

    为了正确追踪潜在的未被处理的拒绝,使用rejectionHandledunhandleRejection 事件就能保持包含这些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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
        let possiblyUnhandledRejections = new Map();
    // 当一个拒绝未被处理,将其添加到 map
    process.on("unhandledRejection", function(reason, promise) {
    possiblyUnhandledRejections.set(promise, reason);
    });
    process.on("rejectionHandled", function(promise) {
    possiblyUnhandledRejections.delete(promise);

    });
    setInterval(function() {
    possiblyUnhandledRejections.forEach(function(reason, promise) {
    console.log(reason.message ? reason.message : reason);
    // 做点事来处理这些拒绝
    handleRejection(promise, reason);
    });
    possiblyUnhandledRejections.clear();
    }, 60000);
    ```

    - - - -
    ##### 浏览器
    浏览器同样能触发两个事件来识别未处理的拒绝。这两个事件由`window`触发,完全等效于Node.js的相关事件:
    * `unhandledrejection`:当一个Promise被拒绝、而在事件循环的一个轮次中没有任何拒绝处理函数被调用,该事件就会被触发;
    * `rejectionHandled`:若一个Promise被拒绝、并在事件循环的一个轮次之后再由拒绝处理函数被调用,该事件就会被触发。

    与Node.js不同的是浏览器只会给事件处理函数一个对象,其包含以下属性:
    * `type`:事件的名称(`unhandlerejection`或`rejectionhandled`);
    * `promise`:被拒绝的Promise对象;
    * `reason`:Promise中的拒绝值(拒绝原因)。
    **拒绝值在两种事件中都可用**。如:
    ```js
    let rejected;

    window.onunhandledrejection = event => {
    console.log(event.type);
    console.log(event.reason.message);
    console.log(rejected === event.promise);
    };

    window.onrejectionhandled = function(event) {

    console.log(event.type);
    console.log(event.reason.message);
    console.log(rejected === event.promise);
    };

    rejected = Promise.reject(new Error("Explosion!"));

    在chrome中受同源策略的影响必须在一个服务上运行

    与之前node中相似的代码

    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
        let possiblyUnhandledRejections = new Map();

    // 当一个拒绝未被处理,将其添加到 map
    window.onunhandledrejection = function(event) {
    possiblyUnhandledRejections.set(event.promise, event.reason);
    };

    window.onrejectionhandled = function(event) {
    possiblyUnhandledRejections.delete(event.promise);
    };
    setInterval(function() {
    possiblyUnhandledRejections.forEach(function(reason, promise) {
    console.log(reason.message ? reason.message : reason);
    // 做点事来处理这些拒绝
    handleRejection(promise, reason);
    });
    possiblyUnhandledRejections.clear();
    }, 1000);
    ```

    - - - -
    ### 串联Promise
    每次对`then()`或`catch()`的调用实际上创建并返回了另一个Promise,仅当前一个Promise被完成或拒绝时,后一个Promise才会被决议。
    ```js
    let p1 = new Promise(function(resolve, reject) {
    resolve(42);
    });

    p1.then(function(value) {
    console.log(value);
    }).then(function() {
    console.log("Finished");
    });
    // 输出为:
    // 42
    // Finished

捕获错误

为了确保能正确处理任意可能发生的错误,应当始终在Promise链尾部添加拒绝处理函数。

在Promise链中返回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
let p1 = function (val) {
// 根据传入的val参数请求第一个接口
return new Promise (function (resolve) {
setTimeout(() => {
resolve({a: val + 1})
}, 300)
}).then(res => {
// 处理第一个接口的数据并传出
return Promise.resolve(res.a);
}).catch(e => console.log(e))
}

let p2 = function (val) {
// 根据第一个接口的数据请求第二个接口
return new Promise (function (resolve) {
setTimeout(() => {
resolve({b: val + 1})
}, 300)
})
.then(res => {
return res.b;
})
}

p1(3).then(p2).then(res => {console.log(res)})

上方展示了此种情况的解决方法,如果存在更多的情况也可以链式调用
有趣的是catch会返回一个resolved状态的promise