0%

JavaScript Promise

简单介绍一下 Promise 以及他的使用、异常处理、同步处理等等…

介绍

  我们都知道 JavaScript 是一种同步编程语言,上一行出错就会影响下一行的执行,但是我们需要数据的时候总不能每次都等上一行执行完成,这时就可以使用回调函数让它像异步编程语言一样工作。
  像 NodeJS 就是采用异步回调的方式来处理需要等待的事件,使得代码会继续往下执行不用在某个地方等待着。但是也有一个不好的地方,当我们有很多回调的时候,比如这个回调执行完需要去执行下个回调,然后接着再执行下个回调,这样就会造成层层嵌套,代码不清晰,很容易进入“回调监狱”。。。
  所以 ES6 新出的 Promise 对象已经 ES7 的 async、await 都可以解决这个问题。
  Promise 是用来处理异步操作的,可以让我们写异步调用的时候写起来更加优雅,更加美观便于阅读。Promise 为承诺的意思,意思是使用 Promise 之后他肯定会给我们答复,无论成功或者失败都会给我们一个答复,所以我们就不用担心他跑了哈哈。
  Promise 有三种状态:pending(未决定),resolved(完成fulfilled),rejected(失败)。只有异步返回时才可以改变其状态,因此我们收到的 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
function promiseTest(boolType = true) {
return new Promise(function (resolve, reject) {
// do something 然后返回一个 Promise 对象
if (boolType) {
resolve('成功');
} else {
reject('失败');
}
});
}
// Promise 的 then 接受两个参数
// 第一个是成功的 resolved 的成功回调
// 另一个是失败的 rejected 的失败回调【可选】。
// 并且 then 也可以返回 Promise 对象,这样就可以实现链式调用。
// 栗子如下
promiseTest(true).then((value) => console.log(`${value}后的处理A`));
promiseTest(false).then(
(value) => console.log(`${value}后的处理B`),
(value) => console.log(`${value}后的处理B`)
);
promiseTest(false).catch((value) => console.log(`${value}后的处理C`));

// 链式调用,这种写法是不是比我们嵌套回调地狱优美多啦~
promiseTest(false)
.catch((value) => promiseTest(true))
.then(() => console.log('第一次调用失败后尝试第二次成功了!'));
// catch 不仅可以捕获失败和 return Promise,也可以捕获异常。
promiseTest(true)
.then((value) => value1)
.catch((e) => console.log(e));

/* ---打印结果--- */
成功后的处理A
失败后的处理B
失败后的处理C
第一次调用失败后尝试第二次成功了!
ReferenceError: value1 is not defined at ...
/* ---打印结果--- */

另外当我们需要在方法中等待 Promise 返回时,需要给方法添加 async 修饰,并使用 await 等待。

1
2
3
4
5
6
7
8
9
async function asyncFunc() { // 只要添加了 async 关键字,该方法的返回值就是一个 Promise。
let result = await new Promise((resolve, reject) => {
setTimeout(() => resolve(123), 2000);
});
return result;
}
asyncFunc(); // Promise {<pending>}
asyncFunc().then((value) => console.log(value)); // 123
await asyncFunc(); // 123

Api 方法

Promise.resolve

将现有对象转为 Promise 对象 resolved,Promise.resolve(‘test’) 相当于 new Promise((resolve) => resolve(‘test’));

Promise.reject

将现有对象转为 Promise 对象 rejected,Promise.rejected(‘test’) 相当于 new Promise((rejected) => rejected(‘test’));

Promise.prototype.then

then() 方法返回一个 Promise,它最多需要有两个参数:Promise 的成功和失败情况的回调函数。

1
2
3
4
5
6
// promiseTest.then(onFulfilled[, onRejected]);
promiseTest.then(value => {
// fulfillment
}, reason => {
// rejection
});
  • onFulfilled 可选
    • 当 Promise 变成接受状态(fulfilled)时调用的函数。该函数有一个参数,即接受的最终结果(the fulfillment value)。
    • 如果该参数不是函数,则会在内部被替换为 (x) => x,即原样返回 promise 最终结果的函数。
  • onRejected 可选
    • 当 Promise 变成拒绝状态(rejected)时调用的函数。该函数有一个参数,即拒绝的原因(rejection reason)。
    • 如果该参数不是函数,则会在内部被替换为一个 “Thrower” 函数 (it throws an error it received as argument)。

Promise.prototype.catch

catch() 方法返回一个 Promise,并且处理拒绝的情况。它的行为与调用 Promise.prototype.then(undefined,onRejected) 相同。事实上调用 obj.catch(onRejected) 其实就是 obj.then(undefined, onRejected)

1
2
3
4
// promiseTest.catch(onRejected);
promiseTest.catch(function(reason) {
// 拒绝/异常处理
});
  • onRejected
    • 当 Promise 被 rejected 时,被调用的一个Function。该函数拥有一个参数:reason/rejection 的原因。
    • 如果 onRejected 抛出一个错误或返回一个本身失败的 Promise,通过 catch() 返回的 Promise 被 rejected。否则,它将显示为成功(resolved)。

Promise.prototype.finally

finally() 方法返回一个 Promise。在 Promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在 then()catch()各写一次 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
promiseTest.finally(() => {
// do my things
});

promiseTest.then(
(result) => {
// do my things
return result;
},
(error) => {
// do my things
throw error;
}
);

Promise.allSettled

该 Promise.allSettled() 方法返回一个在所有给定的 Promise 都已经 fulfilled 或 rejected 后的 Promise,并带有一个对象数组,每个对象表示对应的 Promise 结果。

1
2
3
4
5
6
7
8
9
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('test'), 1000));
const promises = [promise1, promise2];
Promise.allSettled(promises).then((results) => results.forEach((result) => console.log(result.status)));

/* ---打印结果--- */
fulfilled
rejected
/* ---打印结果--- */

Promise.all

Promise.all() 方法接收一个 Promise 的 iterable 类型(Array,Map,Set都属于 ES6 的 iterable 类型)的输入,并且只返回一个 Promise 实例,那个输入的所有 Promise 的 resolve 回调的结果是一个数组。
它的 resolve 回调执行是在所有输入的 Promise 的 resolve 回调都结束,或者输入的 iterable 里没有 Promise 了的时候。
它的 reject 回调执行是只要任何一个输入的 Promise 的 reject 回调执行或者输入不合法的 Promise 就会立即抛出错误,并且 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
/// 当我们需要同步执行多个 Promise 的时候,可以使用 Promise.all() 来"并发请求",减少等待时间。
/// 举个简单的栗子:
/// 假设我需要三次请求获取数据,然后渲染页面。那么我们看一下使用 Promise.all 和不使用的区别。
console.time('不使用Promise.all');
let a = await new Promise((resolve, reject) => {
setTimeout(function () {
// 模拟请求第一笔数据
resolve('123');
}, 1000);
});
let b = await new Promise((resolve, reject) => {
setTimeout(function () {
// 模拟请求第一笔数据
resolve('456');
}, 2000);
});
let c = await new Promise((resolve, reject) => {
setTimeout(function () {
// 模拟请求第一笔数据
resolve('789');
}, 3000);
});
console.log(a, b, c);
console.timeEnd('不使用Promise.all');

console.time('使用Promise.all');
function all() {
return Promise.all([
new Promise((resolve, reject) => {
setTimeout(function () {
resolve('123');
}, 1000);
}),
new Promise((resolve, reject) => {
setTimeout(function () {
resolve('456');
}, 2000);
}),
new Promise((resolve, reject) => {
setTimeout(function () {
resolve('789');
}, 3000);
})
]);
}
console.log(...(await all()));
console.timeEnd('使用Promise.all');

/* ---打印结果--- */
123 456 789
不使用Promise.all: 8569.14794921875 ms
123 456 789
使用Promise.all: 3006.345947265625 ms
/* ---打印结果--- */
  • 我们可以看到,不使用 all 的情况下我们需要等待的时间会长很多,而使用 all 之后,我们的请求相当于并发,大大节约了时间。

Promise.race

Promise.race(iterable) 方法返回一个 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
26
27
28
29
30
31
32
33
34
/// 这个其实就是赛道的意思,哪个 Promise 先完成,就返回哪个。
/// 举个简单的栗子:
/// 假设我们需要从三台服务器上拿取数据,那么那台先返回我们就用哪台的数据。
function race() {
return Promise.race([
new Promise((resolve, reject) => {
// 第一台服务器 1s
setTimeout(function () {
resolve('123');
}, 1000);
}),
new Promise((resolve, reject) => {
// 第一台服务器 2s
setTimeout(function () {
resolve('456');
}, 2000);
}),
new Promise((resolve, reject) => {
// 第一台服务器 3s
setTimeout(function () {
resolve('789');
}, 3000);
})
]);
}
console.time('raceTime');
console.log(await race());
console.timeEnd('raceTime');


/* ---打印结果--- */
123
raceTime: 1056.11083984375 ms
/* ---打印结果--- */

Promise.any

Promise.any() 接收一个 Promise 可迭代对象,只要其中的一个 Promise 成功,就返回那个已经成功的 Promise。如果可迭代对象中没有一个 Promise 成功 (即所有的 Promise 都失败/拒绝),就返回一个失败的 Promise 和 AggregateError 类型的实例它是 Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和 Promise.all() 是相反的。

注意:Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。它当前处于 TC39 第四阶段草案。

  Promise.any() 与 Promise.race() 方法不同,Promise.race() 方法主要关注 Promise 是否已解决,而不管其被解决(成功)还是被拒绝(失败)。所以使用 Promise.any 来获取多台服务器数据时会更合理。

优雅的进行异常处理

详解

  • 之前刷视频有看到一些小问题:
    • 使用多个 await 时,前一个出现异常,如何不影响后续执行?
    • 我们每次使用 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
/// 我们先定义几个函数来测试
function test1() {
return new Promise((resolve, reject) => {
setTimeout(function () {
console.log('test1');
resolve('test1');
}, 1000); // 正常 1s 执行完毕并成功
});
}

function test2() {
return new Promise((resolve, reject) => {
var x = abc + 1; // 出现异常的情况
console.log('test2');
resolve('test2');
});
}

function test3() {
return new Promise((resolve, reject) => {
setTimeout(function () {
try {
var y = abcabc + 1;
resolve(y);
} catch (e) {
console.log('不属于 Promise 内部错误,请自己包裹。');
console.log('不包裹则会冒泡到 window.onerror,若再未处理则报错到控制台。示例:test4!');
reject('test3 error');
}
}, 1000);
});
}

function test4() {
return new Promise((resolve, reject) => {
setTimeout(function () {
var z = abcabcabc + 1;
console.log(z);
}, 1000);
reject('test4 error');
});
}
  • 首先我们看第一个问题,如果我们直接这样执行,那么由于 test2() 出现错误,test1() 肯定是无法执行的。
1
2
await test2();
await test1();
  • 这时候我们需要这样写,但是这样虽然可以解决这个问题,但是如果前面的 Promise 数量一多,那么可读性就大大降低了!
1
2
3
4
5
6
7
8
9
await test2().catch((e) => console.log(e));
await test1();

try {
await test2();
} catch (e) {
console.log(e);
}
await test1();
  • 再结合后面两个问题,我查看了一些资料,包括 Dima Grossman 的 to.js,所以我们可以采用终极方案,话不多说直接上代码。
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
/**
* 首先我参考了 to.js,扩展 Promise 原型方法,用来直接帮助执行且处理异常。
* @param {Function} res
* @param {Function} rej
* @returns
*/
Promise.prototype.to = function (res, rej) {
return this.then((data) => {
res && res(data);
// console.log(data);
return data;
}).catch((err) => {
rej && rej(err); //可去除此行,全局定义处理错误函数,用以解决第三个问题。
console.log(err); // 如果没定义前面的 rej 回调处理函数,我们可以帮助处理,例如此处可以帮我们处理 test2 的异常。
});
};
/**
* 全局捕获异常
* @param {object} message
* @param {object} source
* @param {object} lineno
* @param {object} colno
* @param {object} error
* @returns
*/
window.onerror = function (message, source, lineno, colno, error) {
console.log('捕获到异常:', { message, source, lineno, colno, error });
//do something 全局处理
return true; // return true 不在控制台报错
};
/// 这个可以帮助我们捕获 test4 setTimeout 中的异步异常。

此时我们再如此执行,均不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
await test1();
await test2();
await test3();
await test4();
console.log('前面报错不会执行');

test1();
test2();
test3();
test4();
console.log('前面报错不会执行');

await test1().to();
await test2().to();
await test3().to();
await test4().to();
console.log('前面报错依然会执行');

test1().to((x) => console.log(`自定义处理的${x}`)); // 如果需要自定义处理也可以传入回调函数,我们的扩展 to 原型方法跟 then 一样是支持两个参数的。
test2().to();
test3().to();
test4().to();
console.log('前面报错依然会执行');

多说几句

另外补充一下,说到 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
function request1() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('result1');
}, 1000);
});
}

function request2(need1) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(need1 + 'result2');
}, 1000);
});
}

function request3(need2) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(need2 + 'result3');
}, 1000);
});
}

request1().then((res1) => {
request2(res1).then((res2) => {
request3(res2).then((res3) => {
console.log(res3);
});
});
});
// 这种写法可读性太差且不好维护

  而应该是每次调用 then 方法后,在 then 方法中 return 下一次需要用到的数据。然后 then 方法会返回一个 Promise 实例,再继续使用 then 通过 res 参数可以获取上一次 return 的数据,并在该 then 方法中发送后续的异步请求,这样就达到了我们之前说过的链式调用传递效果,而且 reject 抛出错误的时候,只需在最后 catch 一层就可以了,这样无论是哪个 then reject 了,都会在最后的 catch 这里捕获到错误

1
2
3
4
5
6
request1()
.then((res1) => request2(res1))
.then((res2) => request3(res2))
.then((res3) => console.log(res3))
.catch((e) => console.log('异常处理', e));
// 没错就是这样,作为强迫症程序员,就是要优雅(*v*)!

提一下 yield*

参考文章,虽然与本文无关,但是记录一下。

  yield * 表达式用于委托给另一个 generator 或可迭代对象。表达式迭代操作数,并产生它返回的每个值。我们可以看成使用此关键字让方法一步步执行,他会返回一个对象包含 value(返回值) 和 done(是否完成)。

  • 栗子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* yieldFunc(a, b, c) {
yield* [4, 5, 6];
yield* arguments;
console.log('打印参数后的第一步');
yield 'hello world';
console.log('即将结束');
yield '下一步结束';
console.log('结束');
}

let runFuncs = yieldFunc();

runFuncs.next(); // {value: 4, done: false}
runFuncs.next(); // {value: 5, done: false}
runFuncs.next(); // {value: 6, done: false}
runFuncs.next(); // {value: 1, done: false}
runFuncs.next(); // {value: 2, done: false}
runFuncs.next(); // {value: 3, done: false}
runFuncs.next(); // 打印参数后的第一步,{value: "hello world", done: false}
runFuncs.next(); // 即将结束,{value: "下一步结束", done: false}
runFuncs.next(); // 结束,{value: undefined, done: true}

经验法则

  • 使用异步或阻塞代码时,请使用 Promise。
  • 为了代码的可读性,resolve 方法对应 then, reject 对应 catch。
  • 确保同时写入 .catch.then 方法来实现所有的 Promise。
  • 如果在 resolve/reject 两种情况下都需要做一些事情,请使用 .finally
  • 我们每次改变单个 Promise (单一原则)。
  • 我们可以在一个 Promise 中添加多个处理程序。
  • Promise 对象中所有方法的返回类型,无论是静态方法还是原型方法,都是 Promise。
  • Promise.all 中,无论哪个 Promise 首先未完成,Promise 的顺序都保持在值变量中。

基础部分参考公众号:前端小智

bulb