异步函数 | Promise基础和实例方法

ES6增加了对Promise/A+规范的完善支持,即Promise实例

Promise基础

1
new Promise(executor)

Promise可以通过new操作符来实例化,创建新Promise的时候需要传入执行器(executor)函数作为参数

1
2
let p = new Promise(() => {});   // 这里使用一个空函数进行模拟
console.log(p) // Promise <pending>

Promise状态

  • Promise是一个有状态的对象,Promise可能处于如下状态

    • 待定(pending)
    • 兑现(fulfilled或者rosolved)
    • 拒绝(rejected)
  • Promise的初始状态是Pending,在Pending状态下,Promise的状态可变为fulfilled或者rejected

  • Promise的状态一经改变不可逆!!!

  • Promise的状态是私有的,是不能直接通过JavaScript检测到的(这主要是为了避免根据读取到的Promise状态,以同步方式处理Promise对象)

  • Promise的也不能为外部JavaScript代码修改(Promise故意将异步行为封装起来,从而隔离外部的同步代码)

Promise用途

(1)改变状态

Promise可以用于抽象地表示一个异步操作,Promise的状态可以表示Promise是否完成,Promise的状态一经改变不可撤销和逆转

1
2
3
4
5
6
let p = new Promise((resolve, reject) => {
resolve();
rejece(); // 没有效果
});

console.log(p); // Promise {<fulfilled>: undefined}

为了避免Promise卡在某个状态,可以添加一个定时器退出功能

1
2
3
4
5
6
7
8
9
10
11
let p = new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 10000); // 10秒后调用reject()
// 执行函数的逻辑
});

setTimeout(() => console.log(p), 0); // Promise {<pending>}
setTimeout(() => console.log(p), 11000);
// 10秒后输出 Uncaught (in promise)
// 11秒后输出 Promise {<rejected>: undefined}

因为期约的状态只能改变一次,所以这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的最长时间

如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败(Nice!)

(2)回调处理

Promise封装的异步操作会实际生成某个值,而程序期待Promise状态改变时可以访问这个值,相应地,如果Promise被拒绝,程序就会期待Promise状态改变时可以拿到拒绝的理由

每个Promise只要状态改变为fulfilled,就会有一个私有的内部(value)

每个Promise只要状态改变为rejected,就会有一个私有的内部理由(reason)

无论是值还是理由,都是包含原始值或对象的不可修改的引用,二者都是可选的,而且默认值为undefined

在Promise到达某个落定状态时执行的异步代码始终会收到这个值或理由

通过执行函数控制Promise状态

因为Promise的状态是私有的,所以只能在内部进行操作, 内部操作在Promise的执行器函数中完成

执行器函数主要有两大职责:

  • 初始化Promise的异步行为
  • 控制状态的最终转换【调用resolve()和reject()实现
1
2
3
4
5
let p1 = new Promise((resolve, reject) => resolve());
console.log(p1); // Promise {<fulfilled>: undefined} 因为resolve并没有传入值,所以为undefined

let p2 = new Promise((resolve, reject) => reject());
console.log(p2); // Promise {<rejected>: undefined}
  • 执行器函数是同步执行的,因为执行器函数是Promise的初始化状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
new Promise(() => setTimeout(() => {
console.log('executor');
}, 0))

setTimeout(() => console.log('promise initialized'), 0)

/*
如果Promise是同步执行的,打印结果为:
executor
promise initialized

如果Promise是异步执行的,打印结果为:
promise initialized
executor
*/


/*
实际打印结果为:
executor
promise initialized
*/
  • 添加setTimeout可以推迟切换状态
1
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));

Promise静态方法

(1)Promise.resolve()

Promise.resolve()可以实例化一个解决的Promise

1
2
3
// p1 和 p2 是等价的
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
  • Promise.resolve()的参数对应着解决的Promise的值,Promise.resolve()可以把任何值都转换为一个Promise,传入的参数可能会有如下情况

    • 定值 => 解析或者包装后的Promise对象
    • Promise对象 => Promise对象本身(相当于一个空包装)【幂等】
    • thenable => 返回的Promise会跟随着thenable对象,采用它的最终状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 定值
    let p1 = Promise.resolve(2);
    console.log(p1); // Promise {<fulfilled>: 2}

    // 多余的参数会忽略
    let p2 = Promise.resolve(4);
    console.log(p2); // Promise {<fulfilled>: 4}

    // 空包装
    let p3 = Promise.resolve(p2);
    console.log(p3 === p2); // true
  • Promise.resolve()可以包装任何非Promise的值,包括错误对象,并将其转换为解决的Promise

1
2
3
4
// 可能会导致不符合预期的行为
let p = Promise.resolve(new Error('foo'));

console.log(p); // Promise {<fulfilled>: Error: foo
  • Promise.resolve()是一个幂等方法
1
2
3
4
5
6
7
let p = Promise.resolve(7);

let p2 = Promise.resolve(p);
let p3 = Promise.resolve(p);

console.log(p === p2); // true
console.log(p === p3); // true

(2)Promise.reject()

Promise.reject()可以实例化一个拒绝的Promise并抛出一个异步错误(这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获)

1
2
3
// 下面两个实例等价
let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();
  • Promise.reject()的参数对应着拒绝的Promise的理由

    1
    2
    let p1 = Promise.rereject(3);
    console.log(p1); // Promise {<rejected>: 3}
  • Promise.reject()不是一个幂等方法,如果给它传一个Promise,这个Promise会成为拒绝的理由

    1
    2
    3
    4
    5
    let p1 = Promise.resolve(3);

    let p2 = Promise.reject(p1);

    console.log(p2); // Uncaught (in promise) Promise {<fulfilled>: 3}

Promise同步、异步执行的二元性

1
2
3
4
5
6
7
8
9
10
11
try {
throw new Error('foo');
} catch(e) {
console.log(e); // Error:foo
}

try {
Promise.reject(new Error('bar'))
} catch(e) {
console.log(e); // Uncaught (in promise) Error: bar
}

第二个try/catch没有捕获到错误是因为它没有通过异步模式捕获错误,这是什么意思呢?前面说过:Promise的状态是私有的,是不能直接通过JavaScript检测到的(这主要是为了避免根据读取到的Promise状态,以同步方式处理Promise对象),拒绝Promise的错误并没有抛出到同步代码的线程里,所以try/catch没有捕获到错误,准确地说,要捕获错误就需要用Promise的方法【下面就开始介绍Promise的方法】

Promise的实例方法

实现Thenable接口

在ECMAScript 暴露的异步结构中,任何对象都有一个then()方法,这个方法被认为实现了Thenable 接口

1
2
3
class MyThenable {
then() {}
}

Promise.prototype.then()

Promise.then()返回一个新的Promise实例

Promise.then()最多接收两个参数(可选):onResloved处理程序onRejected处理程序,分别在Promise进入“兑现”和“拒绝”状态时执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function onResolved(id) {
console.log('resolve', id);
}

function onRejceted(id) {
console.log('reject', id);
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve(), 3000))
let p2 = new Promise((resolve, reject) => setTimeout(reject(), 3000))

p1.then(() => onResolved('p1'),
onRejceted('p1'))

p2.then(() => onResolved('p2'),
onRejceted('p2'))

// 3秒后
// resolve p1
// reject p2

因为Promise的状态不可撤销,所以这两个方法一定是互斥

  • 传给then()的任何非函数类型的参数都会被忽略
  • 如果只想提供onRejected参数,那么就要在onResolved的位置写上null或者undefined

(1)onResolve处理程序

  1. Promise.then()返回一个新的Promise实例,这个新Promise的实例是基于onResolved处理程序的返回值构建的,也就是说,该处理程序的返回值会通过Promise.resolve()去包装来生成新的Promise实例

  2. 那么onResolve()处理程序在不同情况下,Promise新实例的情况也不同,主要分为以下几种情况:

    • 没有提供onResolve处理程序 => 会沿用上一个Promise

    • 提供onResolved处理程序但是没有显式的返回语句 => Promise.resolve()会包装默认返回值为undefined

    • 提供onResolved处理程序且有有显式的返回语句 => Promise.resolve()会包装这个【根据不同值的情况,具体之前讲过了,可以看Promise.resolve()那块】

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
let p1 = Promise.resolve(3);    // 此时p1 为  Promise: {<fulfilled>: 3}

// 没有onResolved处理程序
let p2 = p1.then();

// 这个打印p2,必须套上setTimeout,不知道为啥的请自觉补课Even loop
setTimeout(() => {
console.log(p2);
}) // 会沿用上一个Promise p2 为 Promise: {<fulfilled>: 3}

// 提供onResolved处理程序但是没有显式的返回语句 ,Promise.resolve()会包装默认返回值为undefined
let p3 = p1.then(() => {
console.log(3);
// 没有return
}, null);
setTimeout(() => {
console.log(p3);
}) // 值为undefined p3 为 Promise: {<fulfilled>: undefined}

// 提供onResolved处理程序且有有显式的返回语句, Promise.resolve()会包装这个值
let p4 = p1.then(() => 4, null);
setTimeout(() => {
console.log(p4);
}) // 值为4 p4 为 Promise: {<fulfilled>: 4}

let p5 = p1.then(() => Promose.resolve(5), null);
setTimeout(() => {
console.log(p5);
})// 值为5 p5 为 Promise: {<fulfilled>: 5}
  1. onResolved处理程序遇到错误,分为两种情况

    • 抛出异常 => 返回拒绝的Promise

    • 返回错误值 => 不会拒绝而是会把错误值包装成一个解决的Promise

1
2
3
4
5
let p1 = Promise.resolve(1);

let p2 = p1.then(() => {throw new Error(2)}); // Uncaught (in promise) Error: 2

let p3 = p1.then(() => new Error(3)); // p3是一个新的Promise Promise {<fulfilled>: new Error(3)}

(2)onReject处理程序

  1. onRejected处理程序返回的值也会被Promise.resolve()包装,在捕获错误后不抛出异常,而是返回一个解决的Promise
  2. onRejeced处理程序在不同情况下,Promise新实例的情况也不同,主要分为以下几种情况:
    • 没有提供onRejected处理程序 => 会沿用上一个Promise
    • 提供onRejected处理程序但是没有显式的返回语句 => Promise.resolve()会包装默认返回值为undefined
    • 提供onRejected处理程序且有有显式的返回语句 =>Promise.resolve()会包装这个原因
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
let p1 = Promise.reject(3);    // 此时p1 为  Promise: {<rejected>: 3}

// 没有onRejected处理程序
let p2 = p1.then();

setTimeout(() => {
console.log(p2);
}) // 会沿用上一个Promise p2 为 Promise: {<rejected>: 3}

// 提供onRejected处理程序但是没有显式的返回语句 , Promise.resolve()会包装默认返回值为undefined
let p3 = p1.then(null, () => {
console.log(3);
// 没有return
});
setTimeout(() => {
console.log(p3);
}) // 原因为undefined p3 为 Promise: {<fulfilled>: undefined}

// 提供onRejected处理程序且有有显式的返回语句, Promise.resolve()会包装这个原因
let p4 = p1.then(null, () => 4);
setTimeout(() => {
console.log(p4);
}) // 原因为4 p4 为 Promise: {<fulfilled>: 4}

let p5 = p1.then(null, () => Error(5));
setTimeout(() => {
console.log(p5);
})// 原因为5 p5 为 Promise: {<fulfilled>: Error(5)}

补课:手写原理 | Promise.then

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序

这个方法只接收一个参数:onRejected 处理程序

事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)

1
2
3
4
5
6
7
8
let p1 = Promise.reject();
let onRejected = function(e) {
setTimeout(() => console.log('rejected'), 0)
}

// 下面两者处理是一样的
let p2 = p1.then(null, onRejected); // rejected
let p3 = p1.catch(onRejected); // rejected

Promise.catch()返回一个新的Promise实例

1
2
3
4
5
6
7
8
9
10
11
12
let p1 = new Promise(() => {});
let p2 = p1.catch();

setTimeout(() => {
console.log(p1) // Promise {<pending>}
}, 0);

setTimeout(() => {
console.log(p2) // Promise {<pending>}
}, 0);

console.log(p1 === p2); // false

在返回新期约实例方面,Promise.prototype.catch()的行为与Promise.prototype.then()onRejected 处理程序是一样的

Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约添加onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行

  1. onFinally 处理程序无法知道Promise的状态是fulfilled还是rejected,所以这个方法主要用于清理代码
1
2
3
4
5
6
7
8
9
10
11
let p1 = Promise.resolve();
let p2 = Promise.reject();

let onFinally = function() {
setTimeout(() => {
console.log('Finally')
}, 0);
}

p1.finally(onFinally); // 'Finally'
p2.finally(onFinally); // 'Finally'
  1. Promise.prototype.finally()返回一个新的Promise实例
1
2
3
4
5
let p1 = Promise.resolve();

let p2 = p1.finally();

console.log(p1 === p2); // false;
  1. onFinally 处理程序和 onResolved和onRejected不同,因为onFinally被设计为一个状态无关的方法,所以在大多数情况下它将表现为父Promise的传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let p1 = Promise.resolve('foo');

// 以下的情况都会往后面传
let p2 = p1.finally();
let p3 = p1.finally(() => {});
let p4 = p1.finally(() => undefined);
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('qux'));

// 打印结果
setTimeout(() => console.log(p2), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p3), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p4), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p5), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p6), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p7), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p8), 0); // Promise <resolved>: foo
setTimeout(() => console.log(p9), 0); // Promise <resolved>: foo
  1. 如果返回的是一个待定的Promise,或者onFinally处理程序抛出了错误(显式抛出或返回了一个拒绝期约),则会返回相应的期约(待定或拒绝)
1
2
3
4
5
6
7
8
9
10
11
12
13
let p1 = Promise.resolve('foo');

// Promise.resolve()保留返回的期约
let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined

setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined

let p11 = p1.finally(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: baz

非重入Promise方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行

跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行

即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的

这个特性由JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性

感觉这部分只要搞清楚Even Loop就可以了,所以这里略过~

邻近处理程序的执行顺序

如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行

无论是then()、catch()还是finally()添加的处理程序都是如此

这个执行顺序主要是遵循了队列的先进先出原则,掠过~

传递解决值和拒绝理由

到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理程序

拿到返回值后,就可以进一步对这个值进行操作

在执行函数中,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的,然后,这些值又会传给它们各自的处理程序,作为onResolved 或onRejected 处理程序的唯一参数

把这张图理顺~问题不大

拒绝Promise和拒绝错误处理

拒绝期约类似于throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理

  1. 在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由
1
2
3
4
5
6
7
8
9
let p1 = new Promise((resolve, reject) => reject(Error('foo')));
let p2 = new Promise((resolve, reject) => { throw Error('foo'); });
let p3 = Promise.resolve().then(() => { throw Error('foo'); });
let p4 = Promise.reject(Error('foo'));

setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo
  1. 期约可以以任何理由拒绝,包括undefined,但最好统一使用错误对象,这样做主要是因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的

  2. 异步错误只能通过异步的onRejected处理程序进行处理【还记得之前说的:Promise的状态是私有的】

1
2
3
4
5
6
7
// 不正确
Promise.reject(Error('foo')).catch(e => {})

// 不正确
try {
Promise.reject(Error('foo'))
} catch(e) {}
  1. then()catch()onRejected 处理程序在语义上相当于try/catch,出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行,为此,onRejected 处理程序的任务应该是在捕获异步错误之后返回一个解决的期约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log('begin synchronous execution');
try {
throw Error('foo');
} catch(e) {
console.log('caught error', e);
}
console.log('continue synchronous execution');
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution


new Promise((resolve, reject) => {
console.log('begin asynchronous execution');
reject(Error('bar'));
}).catch((e) => {
console.log('caught error', e);
}).then(() => {
console.log('continue asynchronous execution');
});
// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution

手写原理 | Promise.finally

知识补充

手写原理 | Promise

手写原理 | Promise.then

手写原理 | Promise.finally

参考

Promises/A+