如何获取异步操作的结果?
传统方案是使用回调函数,在异步操作结束之后,执行回调函数,并将结果作为参数传递出去。
回调函数
但是回调函数会有两个缺点:
1. 回调地狱
例如:异步方法要依赖前一次结束,所以只能在对方的回调函数里面执行。
fn1(function(value1){
fn2(value1, function(value2){
fn3(value2, function(value3){
fn4(value3, function)(value4){
//...
}
})
})
})
这样就造成了一层层回调函数嵌套,也就是回调地狱,让函数可读性和可维护性变得非常差。
2. 并行问题
例如:需要等待三个接口都请求成功之后,再去执行某个操作,怎么知道三个接口什么时候全部请求成功呢?
(1)对三个接口分别设置三个不同的变量,执行成功后修改变量的值,在每个接口判断其他两个变量值
let isAjaxASuccess = false,
isAjaxBSuccess = false,
isAjaxCSuccess = false;
function ajaxA (callback) {
// 请求成功后更新状态
isAjaxASuccess = true;
if (isAjaxBSuccess && isAjaxCSuccess) {
callback();
}
}
function ajaxB (callback) {
// 请求成功后更新状态
isAjaxBSuccess = true;
if (isAjaxCSuccess && isAjaxCSuccess) {
callback();
}
}
function ajaxC (callback) {
// 请求成功后更新状态
isAjaxCSuccess = true;
if (isAjaxCSuccess && isAjaxBSuccess) {
callback();
}
}
(2)设置 setInterval 进行轮询,直到三个接口都请求成功
let isASuccess = false,
isBSuccess = false,
isCSuccess = false;
function ajaxA () {
// 请求成功后更新状态
isASuccess = true;
}
function ajaxB () {
// 请求成功后更新状态
isBSuccess = true;
}
function ajaxC () {
// 请求成功后更新状态
isCSuccess = true;
}
const interval = setInterval(() => {
if (isASuccess && isBSuccess && isCSuccess) {
callback();
clearInterval(interval);
}
}, 500)
以上两种方法都需要维护每个接口的请求状态,如果后续还需要加接口,代码扩展性很差。
基于以上两个问题,ES6 引入了一种用于处理异步操作的对象 —— Promise 。
Promise
Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 对象有以下两个特点:
- 对象的状态不受外界影响。
Promise 有三种状态:
- 进行中:pending
- 已成功:fulfilled
- 已失败:rejected
let promise = new Promise(function(resolve,reject){
// 异步操作
// 当异步操作成功的时候执行resolve(),就可以将承诺的状态由pending -> fulfilled
// 当异步操作失败的时候执行reject(),就可以将承诺的状态由pending -> rejected
})
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
上面三种状态里面,fulfilled 和 rejected 合在一起称为 resolved(已定型)。代表状态已经凝固,不会再变了,会一直保持这个结果。
为了便于理解,我们通常把 resolved 只指 fulfilled 状态,不包含 rejected 状态。
Promise 构造函数
JavaScript 提供原生的 Promise 构造函数,用来生成 Promise 实例。
const promise = new Promise((resolve, reject) => {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else { /* 异步操作失败 */
reject(new Error());
}
});
上面代码中,Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
-
resolve函数的作用是,将 Promise 实例的状态从 “ 未完成 ” 变为 “ 成功 ”(即从 pending 变为 fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。 -
reject函数的作用是,将 Promise 实例的状态从 “ 未完成 ” 变为 “ 失败 ”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise.prototype.then()
Promise.prototype.then(resolved_callback, rejected_callback)
-
resolved_callback:当承诺对象状态 pending -> resolved 时的回调函数
-
rejected_callback: 当承诺对象状态 pending -> rejected 时的回调函数(该参数可以省略)
每一个 .then() 方法还会返回一个新生成的 promise 对象,这个对象可被用作链式调用,就像这样:
const promise = new Promise((resolve, reject) => {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else { /* 异步操作失败 */
reject(new Error());
}
});
promise
.then(handleResolvedA)
.then(handleResolvedB)
.then(handleResolvedC, handleRejectedC);
上面代码中,promise 后面有三个 then ,意味依次有三个回调函数,只要前一步的状态变为 fulfilled,就会依次执行紧跟在后面的回调函数。
最后一个 then 方法,handleResolvedC 只能获取 handleResolvedB 的返回值,而 handleRejectedC 可以获取 handleResolvedA、handleResolvedB 任意一个发生的错误。假如 handleResolvedA 的状态变为 rejected,那么 handleResolvedB 不会执行,因为是 resolved 的回调函数。Promise 开始寻找,直到找到第一个为 rejected 的回调函数 handleRejectedC。这就是说,Promise 对象的报错具有传递性。
下面是 Promise 的四种不同写法:
function f1() {
return new Promise(function(resolve, reject) {
resolve('f1 运行结果');
return;
})
}
function f2(params) {
console.log('f2 的参数是:', params);
return 'f2 运行结果';
}
function f3(params) {
console.log('f3 的参数是:', params);
}
// 写法一
f1().then(function () {
return f2();
}).then(f3);
// f3 的参数是 f2 运行的结果
// 写法二
f1().then(function () {
f2();
return;
}).then(f3);
// f3 的参数是 undefined
// 写法三
f1().then(f2()).then(f3);
// f3 的参数是 f2 返回的函数的运行结果
// 写法四
f1().then(f2).then(f3);
// f3 的参数是 f2 运行的结果,f2 的参数是 f1 运行的结果
上面代码中,
写法一:f1 的 .then 方法的回调函数返回了 f2 的运行结果,这个结果作为下一个 .then 方法的回调函数的参数,也就是 f3 的参数;
写法二:f1 的 .then 方法的回调函数运行完 f2 之后,直接 return ,所以返回值是 undefined,下一个 .then 方法的回调函数 f3 的参数也就是 undefined;
写法三:f1 的 .then 方法会新生成 Promise 对象,即 f2 运行之后返回的函数,这个 Promise 运行的的结果就是是 f3 的参数;
写法四:f1 的运行之后返回的结果作为它的 .then 方法的回调函数的参数,所以 f2 的参数是 f1 运行的结果,f3 的参数是 f2 运行的结果
Promise.prototype.catch()
Promise.prototype.catch(rejected_callback)
- rejected_callback:当承诺对象的状态pending -> rejected 时的回调函数
不管是ajax交互中出现的404、500异常会被 catch 捕获,在 then 中的代码语法错误也会被 catch 捕获。
Promise.prototype.catch() 方法是 .then(null, rejection) 或 .then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。
promise.then(function(res) {
// ...
}).catch(function(error) {
// 处理 promise 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
上面代码中,promise 方法返回一个 Promise 对象,
- 如果该对象状态变为 resolved,则会调用
then()方法指定的回调函数; - 如果异步操作抛出错误,状态就会变为 rejected,就会调用
catch()方法指定的回调函数,处理这个错误。
另外,then() 方法指定的回调函数,如果运行中抛出错误,也会被 catch() 方法捕获。
一般来说,不要在 then() 方法里面定义 reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法。
// bad
promise.then(function(res) {
// success
}, function(err) {
// error
})
// good
promise.then(function(data) {
// success
})
.catch(function(err) {
// error
});
上面代码中,第二种写法可以捕获前面 then 方法执行中的错误,也更接近同步的写法。因此,建议总是使用 catch() 方法,而不使用 then() 方法的第二个参数。
Promise.prototype.finally()
Promise.prototype.finally(callback)
不管承诺的状态最终如何,该函数中的回调函数都会执行
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管 promise 最后的状态,在执行完 then 或 catch 指定的回调函数以后,都会执行 finally 方法指定的回调函数。
finally 方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是 fulfilled 还是 rejected。这表明,finally 方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
finally 本质上是 then 方法的特例:
promise
.finally(() => {
// ...
});
// 等同于
promise
.then(
result => {
// ...
return result;
},
error => {
// ...
throw error;
}
);
上面代码中,如果不使用 finally 方法,同样的语句需要为成功和失败两种情况各写一次。有了 finally 方法,则只需要写一次。
Promise.all()
Promise.all([promise1, promise2, promise3,...])
Promise.all() 方法接受一个数组(可以不是数组,但必须具有 Iterator 接口)作为参数,promise1、promise2、promise3 都是 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,p 的状态由 p1、p2、p3 决定,分成两种情况:
(1)当 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
(2)只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
// 生成一个Promise对象的数组
const promiseList = arr.map(function (item) {
return getJSON('/post/' + item.id + ".json");
});
Promise.all(promiseList)
.then(function (res) {
// ...
}).catch(function(err){
// ...
});
上面代码中,promiseList 是 Promise 实例的数组。
-
只有实例的状态都变成 fulfilled,才会调用
Promise.all方法后面的.then方法的回调函数 -
或者其中有一个变为 rejected,才会调用
Promise.all方法后面.catch方法的回调函数
注意,如果作为参数的 Promise 实例,自己定义了
catch方法,那么它一旦被reject,并不会触发Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
上面代码中,p1 会 resolved,p2 首先会 rejected,但是 p2 有自己的 catch 方法,该方法返回的是一个新的 Promise 实例,p2 指向的实际上是这个实例。该实例执行完 catch 方法后,也会变成 resolved,导致 Promise.all() 方法参数里面的两个实例都会 resolved,因此会调用 then 方法指定的回调函数,而不会调用 catch 方法指定的回调函数。
如果 p2 没有自己的 catch 方法,就会调用 Promise.all() 的 catch 方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
Promise.race()
Promise.race([promise1, promise2, promise3,...])
Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
例:如果指定时间内没有获得结果,就将 Promise 的状态变为 reject,否则变为 resolve。
const p = Promise.race([
getJSON('/post'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内 getJSON 方法无法返回结果,p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数。
Promise.allSettled()
Promise.allSettled([promise1, promise2, promise3,...])
Promise.allSettled() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected,包装实例才会结束。
该方法返回的新的 Promise 实例,一旦结束,状态总是 fulfilled,不会变成 rejected。状态变成fulfilled 后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入 Promise.allSettled() 的 Promise 实例。
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
上面代码中,Promise.allSettled() 的返回值 allSettledPromise,状态只可能变成 fulfilled。
它的监听函数接收到的参数是数组 results。该数组的每个成员都是一个对象,对应传入 Promise.allSettled() 的两个 Promise 实例。每个对象都有 status 属性,该属性的值只可能是字符串 fulfilled 或字符串 rejected。fulfilled 时,对象有 value 属性,rejected 时有 reason 属性,对应两种状态的返回值。
微任务
Promise 的回调函数属于异步任务,会在同步任务之后执行。
new Promise(function (resolve, reject) {
resolve(1);
}).then(console.log);
console.log(2);
// 2
// 1
上面代码会先输出2,再输出1。因为 console.log(2) 是同步任务,而 then 的回调函数属于异步任务,一定晚于同步任务执行。
异步任务分为:
-
宏任务:setTimeout, setInterval,...
-
微任务:Promises,...
宏任务、微任务的执行顺序
- 先执行同步代码,
- 遇到异步宏任务则将异步宏任务放入宏任务队列中,
- 遇到异步微任务则将异步微任务放入微任务队列中,
- 当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,
- 微任务执行完毕后再将异步宏任务从队列中调入主线程执行,
- 一直循环直至所有任务执行完毕。
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
console.log(2)
resolve(3);
}).then(console.log);
console.log(4);
// 2
// 4
// 3
// 1
上面代码,
- 遇到
setTimeout,异步宏任务,放入宏任务队列中; - 遇到
new Promise,Promise 在实例化的过程中所执行的代码都是同步进行的,所以输出 2; Promise.then中指定的回调是异步执行的,将其放入微任务队列中;- 遇到同步任务
console.log('4');输出 4;主线程中同步任务执行完; - 从微任务队列中取出任务到主线程中,输出 3,微任务队列为空;
- 从宏任务队列中取出任务到主线程中,输出 1,宏任务队列为空,结束