promise

一、 Promise处理异步

1.1 基本用法

1
2
3
4
5
6
7
8
9
const promise = new Promise(function(resolve, reject) {
setTimeout(() => {
if (/*成功*/){
resolve(value);
} else {
reject(error);
}
})
});
  • Promise 新建后就会立即执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const p = new Promise((resolve, reject) => {
console.log(111);
resolve(444)
console.log(222);
});
p.then((value) => {
console.log(value);
});
console.log(333)

// 输出顺序
111
222
333
444
  • 异步加载图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function loadImageAsync (url) {
return new Promise(function (resolve, reject) {
const image = new Image()
// 加载成功后resolve
image.onload = function() {
resolve(image)
}
image.onerror = function() {
reject(new Error('Could not load image at ' + url))
}

image.src = url
})
}

1.2 执行情况

  • p1 3秒之后变为 rejected

  • p2 1秒之后变为 resolve, 值为 p1,p2返回的是另一个 Promise,导致p2自己的状态无效化,由p1的状态决定p2的状态

  • 因此后面的then语句都是针对 p1。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数

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

const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})

p2
.then(result => console.log(result))
.catch(error => console.log(error))

1.2.2 Promise 在resolve后,再抛出错误,不会被捕获,因为 Promise 的状态一旦改变,就永久保持该状态

  • resolve或reject后面的代码依然可以执行
1
2
3
4
5
6
7
8
const promise = new Promise(function(resolve, reject) {
resolve('ok');
console.log('ffff')
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });

1.2.3 then的参数不是函数,后面链式调用依然可以正常执行

1
2
3
4
5
6
7
8
9
10
11
12

const p1 = new Promise(function (resolve, reject) {
resolve(1);
});

p1.then(3)
.then((result) => {
console.log(result, "result");
})
.catch((error) => {
console.log(error, "error");
});

1.2.4 当 Promise 更新后,后面在调用依然可以拿到最新的值

1
2
3
4
5
6
7
8
9
10
11
12
13

var promise1 = new Promise(function (resolve, reject) {
resolve("promise 的resolve");
});

promise1.then((res) => {
console.log(res, "1");
});
setTimeout(() => {
promise1.then((res) => {
console.log(res, "2");
});
}, 500);

1.3 Promise.all()

  • PromisePromise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例

  • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组

  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值

  • const p = Promise.all([p1, p2, p3])

1.4 有时需要将现有对象转为 Promise 对象

1
2
3
4
5
Promise.resolve()
Promise.resolve('foo')

// 等价于
new Promise(resolve => resolve('foo'))

1.5 错误处理

普通情况的异步错误捕获

1
2
3
4
5
6
7
8
9
10
11
12
const task = () => {
setTimeout(() => {
throw new Error('error')
})
}
function main() {
try {
task();
} catch(e) {
console.log(e)
}
}
  • then有两个参数,第一个是成功的回调,第二个是错误的回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误处理推荐
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});

// 不推荐这种
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});

// 也不推荐第二个参数,在第一参数内报错不会被第二个参数捕获到
promise.then(function (res) {}, function (err) {})
  • promise 内部的错误不会冒泡出来,只有通过 promise.catch 才可以捕获,所以用 Promise 一定要写 catch

  • promise 内部无论是 reject 或者 throw new Error,都可以通过 catch 回调捕获

1
2
3
4
5
6
7
8
9
10
function main() {
try {
new Promise(() => {
throw new Error('promise1 error')
})
} catch(e) {
// 无法捕获错误
console.log(e.message);
}
}
  • 用 promise 捕获异步错误
1
2
3
4
5
6
7
8
9
10
const p3 = () =>  new Promise((reslove, reject) => {
setTimeout(() => {
reject('async error');
}, 1000)
});

function main() {
p3().catch(e => console.log(e));
}
main();

1.6 finally()

1
2
3
4
5
6
Promise.prototype.finally()  finally方法不管 Promise 对象最后状态如何,都会执行

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···})

1.7 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
var promise1 = new Promise(function (resolve, reject) {
resolve('第一个promise 的resolve')
})

promise1.then(res => {
// promise then的第一参数是成功时执行,第二个是失败时执行
console.log(res, '1')
})
.then(res => {
// 多个then连续调用时,只有第一个then能拿到resolve的值,后续的只是会被执行但是无法获取resolve的值。
console.log(res, '2')
})
.then(res => {
console.log(res, '3')
// then内返回一个Promise对象时,后续的then都是依赖于返回的Promise 对象
return new Promise(function (resolve, reject) {
setTimeout(_ => {
resolve('第二个promise 的resolve')
}, 3000)
// b(new Error('zzzz'))
})
})
.then(res => {
// 该then取到的事上面返回的Promise的resolve的值。
console.log(res, '4')
})
.then(res => {
console.log(res, '5')
})
.catch(res => {
console.log(res, 'error')
})

1.8 Promise 和 setTimeout (宏任务和微任务)

  • 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个

  • 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs

  • macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

  • micro-task大概包括: process.nextTick, Promise, MutationObserver(html5新特性)

  1. 执行栈内执行上下文的同步任务按序执行,执行完即退栈,而当异步任务执行时,该异步任务进入等待状态(不入栈),同时通知线程:当触发该事件时(或该异步操作响应返回时),需向消息队列插入一个事件消息

  2. 当事件触发或响应返回时,线程向消息队列插入该事件消息(包含事件及回调)

  3. 当栈内同步任务执行完毕后,线程从消息队列取出一个事件消息,其对应异步任务(函数)入栈,执行回调函数,如果未绑定回调,这个消息会被丢弃,执行完任务后退栈

  4. 当线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息,首先去拉取micro-task(微任务),直到所有执行完毕,然后循环再次从macro-task(宏任务)开始,这样一直循环下去

  5. 主线程不断重复上面的第4步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setTimeout(function() {
console.log('timeout1');
new Promise(function(resolve) {
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
new Promise((a) => {
a('ffff')
}).then(_ => {
console.log("promise outer")
})
// 输出顺序
promise outer
timeout1
timeout1_then

Promise 源码实现

分析Promise

从上面的基本使用,我们对 Promise 有大概的了解

  • Promise的构造方法接收一个executor(),在new Promise()时就立刻执行这个executor回调

  • resolve 成功执行then,reject 失败 执行catch

  • 拥有 then, catch, resolve, all, finally等方法

  • Promise 变为成功或失败后,状态无法在更改 resolve 后在 rejected 不会被捕获(Promise A+ 规定必须有三个状态 pending,fulfilled,rejected)Promise A+

  • Promise 可以链式调用,而且then, catch, finally 等都是异步执行

  • then(onFulfilled, onRejected) then 接收两个可选参数,一个成功回调,一个失败回调

  • Promise本质是一个状态机,且状态只能为以下三种:Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态),状态的变更是单向的,只能从Pending -> Fulfilled 或 Pending -> Rejected,状态变更不可逆

  • 我们可以直接调用 Promise.all, Promise.reject, Promise.resolve 但是不能 new 之后调用,说明这三个方法是静态方法,不会被实例继承

从上面我们大概构造出一个轮廓

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
class CPromise {
#status;
constructor(executeFn) {
this.#status = "pending";
const resolve = () => {};
const reject = () => {};
executeFn(resolve, reject);
}

// then 方法
then(resFn, rejFn) {}

// catch 方法
catch(rejectFn) {}

// finally 方法
finally(callback) {}

// 静态的resolve 方法
static resolve(value) {}

// 静态的reject 方法
static reject(reason) {}

// 静态all 方法
static all(promiseArr) {}
}

const p = new CPromise((res, rej) => {});

实现then

看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var promise1 = new Promise(function (resolve, reject) {
resolve("promise 的resolve");
});

promise1
.then((res) => {
console.log(res, "1");
})
.then((res) => {
console.log(res, "3");
return 4
})
.then(res => {
console.log(res, "4")
})
promise1.then((res) => {
console.log(res, "2");
});

// 输出
promise 的resolve 1
promise 的resolve 2
undefined "3"
4 "4"

根据上面分析下面四点

  • 异步或者同步调用的 resolve 其值要传递给then的回调第一个参数, reject 值要传递个catch的回调第一个参数

  • 无论调用多少个非链式调用的 then 或 catch 都能正确的取到 resolve 或 reject 传递的值

  • 链式调用时,第二次及以后then无法得到 resolve 的值

  • then 和 catch 本身接收的都是函数参数

  • then可以链式调用并能够拿到上一个then的返回值,说明then返回的还是Promise对象,并且值能正确传递过来

分析上面四个特点,我们可以得知当 resolve 和 reject 时,通知 then 或 catch 接收的参数(是一个函数)调用并更新参数。而这种实现就是常听到的观察者模式。

观察者模式

定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己

  • 一: resolve 或 reject

  • 多: 每个promise.then的参数匿名函数,then方法执行就会收集订阅(该匿名函数)

  • resolve 或 reject 发生改变,更新调用所有的订阅(函数)并传递更新后的值

使用场景: 当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,应考虑观察者模式

经过上面的分析,我们大概写出如下代码

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class CPromise {
#resolveQueue;
#rejectQueue;
constructor(executeFn) {
// 接收订阅的数组,其实是每个then方法接收的函数入参数
this.#resolveQueue = [];
this.#rejectQueue = [];
// 当 resolve 或 reject 时,会触发所有的订阅更新
const resolve = (val) => {
while (this.#resolveQueue.length) {
const callback = this.#resolveQueue.shift();
callback(val);
}
};
const reject = (val) => {
while (this.#rejectQueue.length) {
const callback = this.#rejectQueue.shift();
callback(val);
}
};
executeFn(resolve, reject);
}

then(resFn, rejFn) {
return new CPromise((resolve, reject) => {
const fulfilledFn = (value) => {
try {
// 获取then的返回值
let x = resFn(value);
// 如果是Promise对象,执行一次新的then添加到订阅队列,如果不是,则直接解析
// 这段代码可以仔细思考一下,如果你then返回的Promise对象,后面链式调用的then其实你新实例化对象的then方法了。
// 如果then返回的是新的Promise对象A,这里首先 x instanceof CPromise 为 true, 并执行一次你返回Promise对象A的then收集到订阅队列
// 当你Promise对象A的resolve执行时,执行队列里的函数发布更新,而更新的函数就是当前then方法内自带的实例化Promise的resolve。
x instanceof CPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};
this.#resolveQueue.push(fulfilledFn);

const rejectedFn = (error) => {
try {
let x = rejFn(error);
x instanceof CPromise ? x.then(resolve, reject) : reject(x);
} catch (error) {
reject(error);
}
};
this.#rejectQueue.push(rejectedFn);
});
}
}


// 使用我们自己的CPromise

const p = new CPromise((res, rej) => {
setTimeout(() => {
res(3);
}, 1000);
});

p.then((res) => {
console.log(res, "位置1");
return 8;
})
.then((res) => {
console.log(res, "位置1-1");
return new CPromise((ress, rejj) => {
// 注意这里,我加了异步,如果没有则拿不到7777的值
setTimeout(() => {
ress(7777);
}, 1000);
});
})
.then((res) => {
console.log(res, "位置1-2");
});
p.then((res) => {
console.log(res, "位置2");
});

上面代码,初步来看结果已经符合 Promise 的使用表现了,不过还有两个问题

  • 同步逻辑没处理,如果同步执行,你会发现resolve和reject在then之前执行。还没收集到订阅就触发更新了

  • Promise的状态转换没处理,reject/resolve 后不可 resolve/reject

同步逻辑处理

  • 观察上面代码的这段内容可以知道,如果我在new CPromise 时,同步执行 resolve 方法,此时还没有调用then方法,订阅队列里根本没有订阅内容,所以也不会有数据更新。

  • 但是真正的Promise,不管你同步还是异步,它都能正确更新,而且无需我们resolve时加 setTimeout 处理。

  • 因此,我们可以对 resolve 和 reject 方法进行处理,把更新订阅放到事件队列的最后面执行

1
2
3
4
5
6
7
const resolve = (val) => {
// 同步 resolve,会在then方法执行前出发,由于then还没有触发,订阅队列内并没有收集到订阅
while (this.#resolveQueue.length) {
const callback = this.#resolveQueue.shift();
callback(val);
}
};

变成如下代码,reject 同理

1
2
3
4
5
6
7
8
9
10
const resolve = (val) => {
const exFn = () => {
while (this.#resolveQueue.length) {
const callback = this.#resolveQueue.shift();
callback(val);
}
};
// 这里这是用setTimeout添加到宏任务队列
setTimeout(exFn);
};

状态处理

  • 上面 1.2 执行情况中,当我 resolve 后,即使在 reject ,catch 也不会捕获到错误

  • 当Promise的状态从 pending 变成 fulfilled 或 rejected 后,就不会在发生改变

  • 另外即使 then的参数不是函数,链式调用依然不会受影响

  • 当 Promise 更新后,后面在调用then依然可以拿到最新的值

为了方便测试比较,我们先把 catch 实现,catch和then的第二个参数是一样的,接收一个函数,函数的参数是捕获的错误,因此catch实现起来就比较简单了。

1
2
3
catch(rejectFn) {
return this.then(undefined, rejectFn)
}

考虑上面then的非函数情况,和后续链式调用的正常使用,我们对 then 做进一步处理

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class CPromise {
#resolveQueue;
#rejectQueue;
constructor(executeFn) {
this.#resolveQueue = [];
this.#rejectQueue = [];
const resolve = (val) => {
const exFn = () => {
while (this.#resolveQueue.length) {
const callback = this.#resolveQueue.shift();
callback(val);
}
};
setTimeout(exFn);
};
const reject = (val) => {
const exFn = () => {
while (this.#rejectQueue.length) {
const callback = this.#rejectQueue.shift();
callback(val);
}
};
setTimeout(exFn);
};
executeFn(resolve, reject);
}

then(resFn, rejFn) {
// 不是函数,容错处理,并保证链式调用的正常
if (typeof resFn !== "function") {
resFn = (value) => value;
}
if (typeof rejFn !== "function") {
rejFn = (errors) => errors;
}
return new CPromise((resolve, reject) => {
const fulfilledFn = (value) => {
try {
let x = resFn(value);
x instanceof CPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};
const rejectedFn = (error) => {
try {
let x = rejFn(error);
x instanceof CPromise ? x.then(resolve, reject) : reject(x);
} catch (error) {
reject(error);
}
};
this.#resolveQueue.push(fulfilledFn);
this.#rejectQueue.push(rejectedFn);
});
}

// catch 实现
catch(rejectFn) {
return this.then(undefined, rejectFn);
}
}

用我们实现的 CPromise 去执行下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

const p1 = new CPromise((resolve, reject) => {
resolve(1);
reject(8);
});

p1.then(3)
.then((result) => {
console.log(result, "result");
})
.catch((error) => {
console.log(error, "error");
});

// 输出
1 "result"
8 "error"

很明显我们实现了,then入参非函数也能保证链式调用正常使用,但是 resolve 之后,reject 结果依然被 catch 捕获了,很明显这是不符合 Promise 的特性的,为此我们还需要处理状态变更

pending -> fulfilled

pending -> rejected

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class CPromise {
#status;
#resolveQueue;
#rejectQueue;
#value;
constructor(executeFn) {
this.#status = "pending";
this.#resolveQueue = [];
this.#rejectQueue = [];
this.#value = undefined;
const resolve = (val) => {
const exFn = () => {
if (this.#status !== "pending") return;
this.#status = "fulfilled";
while (this.#resolveQueue.length) {
const callback = this.#resolveQueue.shift();
callback(val);
}
};
setTimeout(exFn);
};
const reject = (val) => {
const exFn = () => {
if (this.#status !== "pending") return;
this.#status = "rejected";
while (this.#rejectQueue.length) {
const callback = this.#rejectQueue.shift();
callback(val);
}
};
setTimeout(exFn);
};
executeFn(resolve, reject);
}

then(resFn, rejFn) {
if (typeof resFn !== "function") {
resFn = (value) => value;
}
if (typeof rejFn !== "function") {
rejFn = (errors) => errors;
}
return new CPromise((resolve, reject) => {
const fulfilledFn = (value) => {
try {
let x = resFn(value);
x instanceof CPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};
const rejectedFn = (error) => {
try {
let x = rejFn(error);
x instanceof CPromise ? x.then(resolve, reject) : reject(x);
} catch (error) {
reject(error);
}
};
switch (this.#status) {
case "pending":
this.#resolveQueue.push(fulfilledFn);
this.#rejectQueue.push(rejectedFn);
break;
case "fulfilled":
fulfilledFn(this.#value);
break;
case "rejected":
rejectedFn(this.#value);
break;
default:
break;
}
});
}

//catch
catch(rejectFn) {
return this.then(undefined, rejectFn);
}
}

到这里我们常用的 Promise 已经完成了。then 和 catch 以及一些表现形式都符合一个真正的Promise。接下来完善其他方法。

Promise.resolve

1
2
3
4
static resolve(value) {
if (value instanceof CPromise) return value;
return new CPromise((resolve) => resolve(value));
}

Promise.reject

1
2
3
static reject(reason) {
return new CPromise((resolve, reject) => reject(reason));
}

Promise.finally

finally()方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。在finally之后,还可以继续then。并且会将值原封不动的传递给后面的then

1
2
3
4
5
6
7
8
9
finally(callback) {
return this.then(
(value) => CPromise.resolve(callback()).then(() => value),
(reason) =>
CPromise.resolve(callback()).then(() => {
throw reason;
})
);
}

Promise.all

  • Promise.all 方法返回一个 Promise 实例

  • 所有的 promise 都 resolve 或参数中不包含 promise 时回调完成

  • 如果有一个 rejected,此实例回调失败,失败原因的是第一个失败 promise 的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static all(pro) {
let index = 0;
let result = [];
return new CPromise((resolve, reject) => {
pro.forEach((p, i) => {
CPromise.resolve(p).then(
(val) => {
index++;
result[i] = val;
if (index === pro.length) {
resolve(result);
}
},
(err) => {
reject(err);
}
);
});
});
}

完整代码

返回
顶部