由于 Js 是一个单线程的语言,而且在设计之初没有考虑到异步操作的需求,因此在后续的时间里,Js 出产了各种各样的异步方案。其中,Promise 就是最著名的也是目前使用最广泛的异步方案。但是 Promise 复杂的链式调用和其中的函数嵌套有时让代码变得非常的复杂,因此在 ES6 中,又出产了 Async/Await 这种方案。
因此我们可以说,Async/Await 就是 Promise 的一种更加简洁的写法,不需要刻意地去进行链式调用。
那么接下来,我们就用一种简单明了地方式讲一下这两种异步编程方式。
首先我们来讲一下 Promise ,它是当前我们使用最多的异步方法。它不仅能够让我们知道单个异步任务当前的进行状态,并且可以捕获多个异步任务并发下的群体请求状态。
首先,我们来了解一下为什么要使用 Promise 。
为何要使用 Promise
上面我们也提起了,Promise 是为了满足 Js 的异步需求才发明的。那么我们通常说的异步,其实就是我们在执行一个任务的时候,通常希望同步地执行另外一个任务,而这个任务的执行结束时间是未知的。我希望能够获取到这个任务的执行时间,并且在其执行完之后通过它返回的数据,继续执行其他的任务。
我们为了满上面这些条件的后半段,旧式方法一般是这样写的:
A(result, function (newResult) {
B(newResult, function (finalResult) {
c(finalResult, function(text) {
console.log(text);
...
});
});
})
上面这种书写方式,我们还可以继续无穷嵌套,最终我们就会陷入著名的 地狱回调。
所以我们使用了 Promise 来简化上面的操作,使用 Promise 写完之后,会变成下面这样:
A().then((result)=> B(result) ).then((newResult) => C(newResult));
我们可以清楚地看到,下面的这种方式比上面的可读性要强得多。而且,通过这个例子,我们也可以看到 Promise 的本质。其实就是将回调函数传入 Promise 中,并返回当前函数的执行结果,使得下一个 then 可以拿到当前函数返回的结果。
因此,使用 Promise 不仅可以帮我们实现异步调用的目标,同时也可以帮我们简化代码,使得代码的过程更加清晰。下面,我们就来具体地看一下 Promise 是如何进行使用的。
如何使用 Promise
我们需要使用 Promise 的一大原因是我们需要捕获异步调用的状态,因为它不像同步的调用栈明确知道什么时候当前的任务可以执行完,所以 Promise 的一大功能就是追踪当前任务的执行状态,那么它是怎么样进行捕获的呢?
在使用 Promise 的时候,我们一般使用以下的方法进行初始化:
let a = new Promise((resolve, reject)=> { tasks });
在上面这段代码中, 我们设定 a 为一个异步任务,在初始化这个构造函数的时候,接收一个 感知函数, 这个函数的作用就是来捕获其中的 tasks 的执行情况的。这个函数接受两个参数 resolve 和 reject, 这两个函数分别表示任务 成功 和 失败 时调用的函数。通过调用这两个函数,我们就可以清楚地知道,当前任务的执行状态是什么样的,从而进行下面的动作。
let a = new Promise((resolve, reject) => {
let res = task();
// resolve(res); // 成功
// 或者
// reject(res); // 失败
});
在这一步,我们可以通过 resolve 函数和 reject 函数来获取到任务执行完之后的确认值,不论是调用哪一个函数,当前的任务都处在一个 fullfill 的状态,也就是已完成的状态,而里面的 res 这个返回值则不是必须的。
当前的任务已完成之后,结束函数已经被调用,那么下一步,我们就需要拿到当前任务的执行结果和返回值。由于我们是要在 a 这个实例的外部拿到状态结果和返回值,因此我们需要链式调用 Promise 实例 上面的函数。
链式调用
在这里,我们调用 Promise 实例 上面的 then 函数来获取当前任务执行的效果。在 then 函数中,我们依旧会传两个函数当做参树,而这两个函数依旧表示 成功 或者 失败 时调用的函数,失败 一般是指 Promise 内部抛出错误。
let a = new Promise((resolve, reject) => {
let res = task();
// resolve(res);
// 或者
// reject(res);
});
a.then(function success (res){ return res; }, function error (rej) { console.log(rej) });
在上面这个 then 函数被调用之后,依旧会返回一个 Promise 对象,因此这个 then 函数之后还可以调用 Promise 对象上的其他函数,包括 then 本身,这就是 Promise 链式调用 的实现原理。
let a = new Promise((resolve, reject) => {
let res = task();
// resolve(res);
// 或者
// reject(res);
});
a.then(function successA (res){ return res; }, function errorA (rej) { console.log(rej)})
.then(function successB (res){ return res; }, function errorB (rej) { console.log(rej)})
.then(function successC (res){ return res; }, function errorC (rej) { console.log(rej)});
但是要注意,then 必须要有返回值,否则下一个调用拿不到上一个调用的结果。
上面我们讲的所有的 Promise 调用链的执行都是异步的,也就是说 then中的操作只有在下一次事件循环中才会被执行。
let a = new Promise((resolve, reject) => {
setTimeout(()=> {
resolve('fail');
console.log('success');
}, 120);
});
a.then((res)=>{
console.log(`异步调用${res}`);
});
// 输出结果
success
VM5529:9 异步调用fail
在这个过程中,首先进入 Promise之后,会执行 setTimeout。 但是由于这个 Promise 的 resolve 函数是在计时器中执行的,因此 then 中的输出结果会在 setTimeout 之后执行。
抛出错误
链式调用固然在处理任务返回状态时非常方便,但是如果其中任意的一个链路点出了问题,抛出错误,我们就需要用 .catch 来捕获错误。这个捕获我们一般会放在整个链式调用的最后,也就是说在整个链式调用期间,只要有错误出现,程序立刻会跳入到 .catch 中。因此,我们可以将上面的代码改写为如下所示:
let a = new Promise((resolve, reject) => {
resolve('this is a');
// 或者
// reject(res);
});
a.then(function successA (res){ console.log('a') })
.then(function successB (res){ throw new Error('test error') })
.then(function successC (res){ console.log('b') })
.catch(function catchError(err) { console.log(err)} );
在这段代码中,最终会输出:
a
VM832:11 Error: test error
at successB (<anonymous>:9:38)
Promise {<fulfilled>: undefined}
在这段输出中,我们可以首先看到第一次调用 then 时输出的 a, 随后我们抛出了一个 error, 这个 error 被最终的 catch 函数捕获,并且输出错误信息。这里面 b 并没有被输出,这也就说明了在链式调用中,如果中间有错误抛出,那么链式调用的过程会被中断。出错之后,这个 Promise 的状态会被更新为 fullfilled。 这个状态标志着当前的 Promsie 状态已经结束。
不过这里需要明确一个点的就是如果链式调用中返回的是一个 Promise,那么这个调用就不会被跳过,我们将上面的这个例子稍微修改一下:
let a = new Promise((resolve, reject) => {
resolve('this is a');
// 或者
// reject(res);
});
let c = new Promise((resolve, reject) => {
console.log('c');
resolve('this is c');
});
a.then(function successA (res){ return res })
.then(function successB (res){ throw new Error('test error') })
.then(function successC (){ return c;})
.catch(function catchError(err) { console.log(err)} );
// 结果
c
VM3114:16 Error: test error
at successB (<anonymous>:14:38)
Promise {<fulfilled>: undefined}
在这里,我们声明了一个新的 Promise 实例,并将它赋值给变量 c。 在这里我们可以看到,虽然我们在 successB 中抛出了错误,但是由于 successC 中使用返回的是一个 Promise 对象,因此在这个过程中会执行其中的逻辑,那么为什么链式调用遇到 Promise 就一定会执行呢。
因为链式调用的 Promise 是使用一个嵌套另外一个的形式去做的,就像下面这样:
(successC, (successB, (successA) ) );
而在执行的时候是通过由内到外的顺序去执行的,也就是按照 A、B、C 这样的顺序去执行的。但是如果当前的某一个节点中返回的是一个 Promise,那么在这个 then 中的逻辑执行完之后,这个 Promise 会被放入原来的 then 函数的位置去立即执行。也就是说 successB 执行完之后,如下所示:
(Promise C);
在上面我们可以看到在 successC 被执行完成之后,由于它返回的是一个 Promise 对象并且这个对象内立即执行了 console.log(c),因此这个 c 会第一个被输出。
当 Promise 报错的时候,也就是它被拒绝的时候,同样会发送一个事件到全局作用域,也就是 window 下面。当 Promise 被拒绝,并且在 reject 函数中处理该事件时,就会触发 rejectionhandled 事件,如果没有在 reject中处理该事件时,就会触发 unhandledrejection 事件。
监听方法如下
window.addEventListener("unhandledrejection", event => {
}, false);
那么在 Promise 失败之后,它后面的 then 函数还会被调用吗?答案是肯定的,即使 Promise 被拒绝了,它后面的 then 还是会被继续调用。
Promise 上的 API
除了 Promise 实例对象上的 API,Promise 对象本身也有很多的函数可以供我们进行组合来实现我们想要的功能。
Promise.all() 和 Promise.race()
这两个 API 都是在处理多个并发异步任务时调用的函数,这两者的区别是 all 会等待所有的异步任务全部执行完毕之后状态会变成 fullfill, 但是 race 则是只要有一个异步任务成功就会变为 fullfill 状态。
Promise.resolve() 和 Promise.reject()
这两个函数和之前提到的 Promise 的 成功 和 失败 方法是一样的,都是表示当前的 Promsie 状态是成功的还是失败的。但是不同的是,resolve 表示的是其中传入的 Promise 的状态,如果什么值都没有传,那就默认表示 fullfill。
Async/Await
说完了 Promise,我们来说说 Async/Await。为什么要把这两个放在一起说呢,因为其实 Async/Await 本质上就是 Promise 的一个语法糖,本质上还是 Promise,之所以用它就是为了使调用过程比 链式调用 更加简洁。
那我们就先来看看它的基础语法是什么样的。
function testAsync(text) {
return new Promise((resolve) => {
resolve(text);
});
}
async function getResult() {
var a = await testAsync('happy');
console.log(a);
}
getResult();
// 输出 happy
从上面我们看出 async 函数是由 async 关键字声明的函数,并且在其中可以使用 await 关键字。await 关键字表示等待一个异步任务的执行,如果当前的任务没有执行完,async 中的其他任务不会被执行,而且在 testAsync 中的解决值,会被当成其返回值,因此会输出 happy。
Async 函数的返回值是一个 Promise,并且其解决值就是它的返回值,我们可以将上面的例子改下:
function testAsync(text) {
return new Promise((resolve) => {
resolve(text);
});
}
async function getResult() {
var a = await testAsync('happy');
return 1;
}
var c = getResult();
console.log(c);
// 输出
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 1
上面的这个 getResult 函数等价于:
function getResult() {
var a = testAsync('happy');
return Promise.resolve(1);
}
在 async 函数中,遇到 await 表达式之前,async 表达式都是同步执行的,直到遇到 await 之后才开始异步执行。我们可以看看下面的这个例子:
async function getResult() {
console.log('a');
}
getResult();
console.log('b');
// 结果 a、b
那么我们如果给里面加一个 await 函数会怎么样呢:
async function getResult() {
const result = await 'a';
console.log(result);
}
getResult();
console.log('b');
// 输出结果 b、a
从上面我们可以看出来,如果我们将 await 的执行结果输出,那么 a 就会在 b 之后打印出来。
我们上面曾经说过,async/await 其实就是 Promise 的一个语法糖,它其实本质上就是 Promsie。那么 async 函数返回的是一个 Promise, await 表达式是什么呢?其实它表示的就是 then 这个函数,async 函数中对于 await 表达式的多次使用实际上就是一个链式调用,我们看下面这个例子:
async function ex() {
const result1 = await new Promise((resolve) => setTimeout(() => resolve('1')));
const result2 = await new Promise((resolve) => setTimeout(() => resolve('2')));
}
ex();
在这个函数中,有两个 await 表达式,那么在实际的运行过程中,就会成为两个 then 函数:
new Promise((resolve, reject) => { resolve() })
.then(resolve => { return new Promise(resolve) => setTimeout(() => resolve('1'))})
.then(resolve => { return new Promise(resolve) => setTimeout(() => resolve('2'))})
要注意的是,await/async 和 Promise 不一样的是在链式调用的生成,Promise 在声明的时候就会生成其链式的调用,但是 async/await 的链式调用则是阶段性生成的。
const resolveAfter2Seconds = function() {
console.log("starting slow promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("slow");
console.log("slow promise is done");
}, 2000);
});
};
const resolveAfter1Second = function() {
console.log("starting fast promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("fast");
console.log("fast promise is done");
}, 1000);
});
};
const sequentialStart = async function() {
console.log('==SEQUENTIAL START==');
// 1. Execution gets here almost instantly
const slow = await resolveAfter2Seconds();
console.log(slow);
const fast = await resolveAfter1Second();
console.log(fast);
}
sequentialStart();
// 输出结果
==SEQUENTIAL START==
starting slow promise
slow promise is done
slow
starting fast promise
fast promise is done
fast
我们在这里分析一下上面的这段代码。首先,调用 sequentialStart 函数之后,会执行同步操作,输出 ==SEQUENTIAL START==。下一步,遇到 await 语句,在这个语句中等待 resolveAfter2Seconds 这个函数的返回值。这个就相当于在 Promise 后添加了一个 then 的链式调用。这个时候,执行权会交给 resolveAfter2Seconds,在这个函数中,首先会遇到一个同步执行的 console,输出 starting slow promise,接着 return 一个 Promise。在这个 Promise 中,有一个等待 2s 的 setTimeout。2s 之后这个 setTimeout 里面的逻辑执行,首先会输出 slow promise is done,然后将这个 Promise 变为 fullfill 状态,返回值为 slow。此时执行权重新交回给 sequentialStart,然后输出 slow。通过上面这个执行过程我们可以发现,在 await 语句执行完之前,是不会继续执行其他的语句的。
在第一个语句执行完之后,我们可以接着执行第二个 await,此时在 Promise 中就会建立其第二个 then 函数。同样这个 await 其中有一个间隔 1s 执行的 setTimeout,执行过程和上方的是相同的。
上面这个例子中的两个 setTimeout 是分成两步被创建的,但是如果这两个 setTimeout 被同时创建,await 又会怎么执行呢:
const resolveAfter2Seconds = function() {
console.log("starting slow promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("slow");
console.log("slow promise is done");
}, 2000);
});
};
const resolveAfter1Second = function() {
console.log("starting fast promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("fast");
console.log("fast promise is done");
}, 1000);
});
};
const concurrentStart = async function() {
console.log('==CONCURRENT START with await==');
const slow = resolveAfter2Seconds();
const fast = resolveAfter1Second();
console.log(await slow);
console.log(await fast);
}
concurrentStart();
// 输出结果
==CONCURRENT START with await==
starting slow promise
starting fast promise
fast promise is done
slow promise is done
slow
fast
在上面的 concurrentStart 函数中,两个函数前后执行,setTimeout 几乎被同时创建。而在 resolveAfter1Second 中的 setTimeout 只需要等待 1s 就会被执行,因此先输出 fast promise is done。而 await 语句依旧是按照先后顺序同步执行的,因此,虽然是 resolveAfter1Second 先被 resolve 掉,但是 fast 依旧会在 slow 之后输出。
在上面的文章中,我们分别讲到了 Promise 和 async/await 的语法,并且讲到了它们之间的联系。async/await 语法目前是在 ES7 中被支持,因此它是面向未来的一种异步书写方式,很值得我们去进行学习。大家有什么对这两种语法的想法也可以在评论区和我交流。