简单介绍一下 Promise 以及他的使用、异常处理、同步处理等等…
我们都知道 JavaScript 是一种同步编程语言,上一行出错就会影响下一行的执行,但是我们需要数据的时候总不能每次都等上一行执行完成,这时就可以使用回调函数让它像异步编程语言一样工作。
像 NodeJS 就是采用异步回调的方式来处理需要等待的事件,使得代码会继续往下执行不用在某个地方等待着。但是也有一个不好的地方,当我们有很多回调的时候,比如这个回调执行完需要去执行下个回调,然后接着再执行下个回调,这样就会造成层层嵌套,代码不清晰,很容易进入“回调监狱”。。。
所以 ES6 新出的 Promise 对象以及 ES7 的 async、await 都可以解决这个问题。
Promise 是用来处理异步操作的,可以让我们写异步调用的时候写起来更加优雅,更加美观便于阅读。Promise 为承诺的意思,意思是使用 Promise 之后他肯定会给我们答复,无论成功或者失败都会给我们一个答复,所以我们就不用担心他跑了哈哈。
Promise 有三种状态:pending(未决定),resolved(完成fulfilled),rejected(失败)。只有异步返回时才可以改变其状态,因此我们收到的 Promise 过程状态一般只有两种:pending->fulfilled 或者 pending->rejected。
直接上代码
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等待。
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将现有对象转为 Promise 对象 resolved,Promise.resolve(‘test’) 相当于 new Promise((resolve) => resolve(‘test’));
将现有对象转为 Promise 对象 rejected,Promise.rejected(‘test’) 相当于 new Promise((rejected) => rejected(‘test’));
then() 方法返回一个 Promise,它最多需要有两个参数:Promise 的成功和失败情况的回调函数。
// promiseTest.then(onFulfilled[, onRejected]);
promiseTest.then(value => {
// fulfillment
}, reason => {
// rejection
});catch() 方法返回一个 Promise,并且处理拒绝的情况。它的行为与调用
Promise.prototype.then(undefined,onRejected)相同。事实上调用obj.catch(onRejected)其实就是obj.then(undefined, onRejected)。
// promiseTest.catch(onRejected);
promiseTest.catch(function(reason) {
// 拒绝/异常处理
});finally() 方法返回一个 Promise。在 Promise 结束时,
无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()和catch()中 各写一次 的情况。
promiseTest.finally(() => {
// do my things
});
promiseTest.then(
(result) => {
// do my things
return result;
},
(error) => {
// do my things
throw error;
}
);该 Promise.allSettled() 方法返回一个在所有给定的 Promise
都已经 fulfilled 或 rejected 后的 Promise,并带有一个对象数组,每个对象表示对应的 Promise 结果。
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 的 iterable 类型
(Array,Map,Set都属于 ES6 的 iterable 类型)的输入,并且只返回一个 Promise 实例,那个输入的所有 Promise 的 resolve 回调的结果是一个数组。 它的resolve回调执行是在所有输入的 Promise 的 resolve 回调都结束,或者输入的 iterable 里没有 Promise 了的时候。 它的reject回调执行是只要任何一个输入的 Promise 的 reject 回调执行或者输入不合法的 Promise 就会立即抛出错误,并且 reject 的是第一个抛出的错误信息。
/// 当我们需要同步执行多个 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
/* ---打印结果--- */Promise.race(iterable) 方法返回一个 Promise,一旦迭代器中的某个 Promise 解决或拒绝,返回的 Promise 就会解决或拒绝。
/// 这个其实就是赛道的意思,哪个 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 可迭代对象,只要其中的一个 Promise 成功,就返回那个已经成功的 Promise。如果可迭代对象中没有一个 Promise 成功 (即所有的 Promise 都失败/拒绝),就返回一个失败的
Promise 和 AggregateError 类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和 Promise.all() 是相反的。
注意:Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。它当前处于 TC39 第四阶段草案。
Promise.any() 与 Promise.race() 方法不同,Promise.race() 方法主要关注 Promise 是否已解决,而不管其被解决(成功)还是被拒绝(失败)。所以使用 Promise.any 来获取多台服务器数据时会更合理。
/// 我们先定义几个函数来测试
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() 肯定是无法执行的。await test2();
await test1();await test2().catch((e) => console.log(e));
await test1();
或
try {
await test2();
} catch (e) {
console.log(e);
}
await test1();/**
* 首先我参考了 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 中的异步异常。此时我们再如此执行,均不会报错。
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 的优雅处理,我们平时写的时候前往不要像下面一样嵌套使用。
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 这里捕获到错误。
request1()
.then((res1) => request2(res1))
.then((res2) => request3(res2))
.then((res3) => console.log(res3))
.catch((e) => console.log('异常处理', e));
// 没错就是这样,作为强迫症程序员,就是要优雅(*v*)!Promise.prototype.retry = function (count = 0, delay = 0) {
return new Promise((resolve, reject) => {
this.then((res) => {
resolve(res);
}).catch(async (e) => {
if (count > 0) {
// 此处也可使用 setTimeout 实现
await Promise.prototype.sleep(delay);
--count;
console.log('重试', count);
resolve(this.retry(count, delay));
} else {
reject('重试结束');
}
});
});
};
Promise.prototype.sleep = function (milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
};
new Promise((resolve, reject) => reject('test')).retry(3, 1000);
参考文章,虽然与本文无关,但是记录一下。
yield * 表达式用于委托给另一个 generator 或可迭代对象。表达式迭代操作数,并产生它返回的每个值。我们可以看成使用此关键字让方法一步步执行,他会返回一个对象包含 value(返回值) 和 done(是否完成)。
function* yieldFunc(a, b, c) {
yield* [4, 5, 6];
yield* arguments;
console.log('打印参数后的第一步');
yield 'hello world';
console.log('即将结束');
yield '下一步结束';
console.log('结束');
}
let runFuncs = yieldFunc(1, 2, 3);
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}
// 假如我们一个验证需要多步,我们可以给 next() 传参,传递的值在原函数体中会变成上步得到的结果。
function* test(a, b) {
const x = yield (a + b);
// x 的值是我们根据第一步的结果判断后,通过 next 传递给他的。
const y = yield x == 2; // 例如此处:xxx.next(6) 则 x = 6; xxx.next(7) 则 x = 7; 而不管我们传递的 a b 是什么。
let z = 'hello world';
if (y) {
console.log('认证成功!');
z = '已登录';
} else {
console.log('认证失败!');
z = '未登录';
}
return z;
}
let authTest = test(1, 1);
let hasNext = authTest.next();
console.log(hasNext);
while (!hasNext.done) {
hasNext = authTest.next(hasNext.value)
console.log(hasNext);
}
// {value: 2, done: false}
// {value: true, done: false}
// 认证成功!
// {value: '已登录', done: true}
let authTestTrue = test(1, 1);
let next = authTestTrue.next();
console.log(next); // {value: 2, done: false}
next = authTestTrue.next(100);
console.log(next); // {value: false, done: false}
next = authTestTrue.next(true);
// 认证成功!
console.log(next); // {value: '已登录', done: true}resolve 方法对应 then, reject 对应 catch。.catch 和 .then 方法来实现所有的 Promise。.finally。Promise.all 中,无论哪个 Promise 首先未完成,Promise 的顺序都保持在值变量中。基础部分参考公众号:前端小智