如何用同步的方式来实现异步编程?
文章介绍了
异步回调
、Promise
、Generator
和async/await
四种异步编程方案,通过分析它们的演进过程,我们可以看到Promise解决了异步回调的Callback Hell
问题;Generator的出现让用同步的方式来实现异步编程
成为了可能,async/await则是结合了Promise和Generator优势的语法糖
。在开发中,我们应该根据实际情况,选择最合适的方案。
关键词:异步回调
、Promise
、Generator
、async/await
、Callback Hell
、异步函数
、回调函数
、异步错误
一. 异步回调
异步回调是最基础的异步编程方案,不足之处是:
-
需要把回调作为参数传给异步函数
。 -
当串联执行多个异步操作时,会出现 Callback Hell
问题。
示例一:连续发出两个请求,并且把第一个请求的结果作为第二个请求的参数。
function request(data,callback){
setTimeout(()=>{
callback(data * 2);
},1000);
}
request(1,(data1)=>{
console.log(data1);
request(data1,(data2)=>{
console.log(data2)
});
})
可以看出,如果串联执行的异步操作越多,将会导致回调函数层层嵌套。
二. Promise
Promise解决Callback Hell
的思路是:将异步操作和回调解耦
。由Promise对象负责跟踪异步操作的完成状态和触发回调函数
。
示例:连续发出两个请求,并且把第一个请求的结果作为第二个请求的参数。
function request(data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data * 2)
}, 1000)
})
}
request(1).then(data1=>{
console.log(data1)
return request(data1);
}).then(data2=>{
console.log(data2);
})
下面通过实现一个简单的Promise类来了解其工作原理。
1. 创建Promise
Promise对象如何跟踪异步操作的完成状态?
Promise构造函数的入参是一个接受resolve和reject两个参数的处理器函数
。在Promise对象被创建且返回新对象之前,处理器函数会被立刻执行
,并传入resolve和reject参数。处理器函数通常用来执行一些异步操作,并在操作完成后调用resolve/reject来变更Promise对象的状态。
const p4 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('执行成功')
},1000);
})
2. 状态变更
输入:
let p0 = new Promise((resolve, reject) => {})
console.log('p0', p0)
let p1 = new Promise((resolve, reject) => {
resolve('成功')
})
console.log('p1', p1)
let p2 = new Promise((resolve, reject) => {
reject('失败')
})
console.log('p2', p2)
let p3 = new Promise((resolve, reject) => {
throw('报错')
})
console.log('p3', p3)
// 刚创建时,p4状态为pending,1000ms后变更为fulfilled。
const p4 = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('执行成功')
},1000);
})
输出:
结论:
-
刚创建的Promise对象处于 pending
状态。 -
当异步操作完成后, 调用resolve/reject变更状态为fulfilled/rejected
。 -
当处理器函数出错( 同步错误
)时,变更状态为rejected。
实现:
class MyPromise {
constructor(executor) {
this.initValue();
// 由于resolve/reject是外部函数executor调用的,所以必须将this硬绑定为当前MyPromise对象
this.initBind();
try {
// 执行传进来的函数
executor(this.resolve, this.reject);
} catch (e) {
// 捕捉到错误直接执行reject
this.reject(e);
}
}
initBind() {
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
}
initValue() {
this.PromiseResult = null;
this.PromiseState = 'pending';
}
resolve(value) {
// 状态只能由pending转换为fulfilled/rejected
if (this.PromiseState == 'pending'){
this.PromiseState = 'fulfilled';
this.PromiseResult = value;
}
}
reject(reason) {
// 状态只能由pending转换为fulfilled/rejected
if (this.PromiseState == 'pending'){
this.PromiseState = 'rejected';
this.PromiseResult = reason;
}
}
}
3. 触发回调
通过Promise对象的then()来注册回调函数。当Promise对象从pending状态变更为fulfilled/rejected状态时,会自动触发回调函数。
输入/输出:
const p1 = new Promise((resolve, reject) => {
resolve('执行成功');
}).then(res => console.log('resolve=',res), err => console.log('reject=',err))
// “立刻”输出 ”resolve=执行成功“
const p2 = new Promise((resolve, reject) => {
reject('执行失败');
}).then(res => console.log('resolve=',res), err => console.log('reject=',err))
// “立刻”输出 ”reject=执行失败“
const p3 = new Promise((resolve, reject) => {
reject('执行失败');
});
p3.then(res => console.log('resolve=',res), err => console.log('reject=',err));
// 输出 ”reject=执行失败“
结论:
-
then接收两个参数: 成功回调
和失败回调
-
当Promise状态为 fulfilled
时执行成功回调
,为rejected
执行失败回调
实现:
-
通过then注册回调时,如果Promise状态为fulfilled/rejected,则执行回调函数; -
如果Promise状态为pending,则先保存回调函数,等异步操作结束后再执行回调。 -
捕获回调函数中的错误。
initValue() {
...
// 保存回调函数。
this.onFulfilledCallback;
this.onRejectedCallback;
}
resolve(value) {
...
// 状态变更为fulfilled,执行保存的成功回调
if (this.onFulfilledCallback) {
this.onFulfilledCallback(this.PromiseResult);
}
}
reject(reason) {
...
// 状态变更为rejected,执行保存的失败回调
if (this.onRejectedCallback) {
this.onRejectedCallback(this.PromiseResult);
}
}
then(onFulfilled, onRejected) {
// 参数校验,确保一定是函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
if (this.PromiseState === 'fulfilled') {
// 执行fulfilled回调
onFulfilled(this.PromiseResult);
} else if (this.PromiseState === 'rejected') {
// 执行rejected回调
onRejected(this.PromiseResult);
}else if (this.PromiseState === 'pending') {
// Promise为pending状态,暂时保存两个回调
this.onFulfilledCallback = onFulfilled;
this.onRejectedCallback = onRejected;
}
}
4. 链式调用
至此,Promise已经实现基本功能:跟踪异步操作完成状态
和触发回调函数
。但还没解决Callback Hell问题。而链式调用允许将进一步的操作与一个变为已完成状态的 Promise对象关联起来。
实现方法就是在then()、catch()和finally()返回一个新的Promise对象。
输入/输出
const p1 = new Promise((resolve, reject) => {
resolve(100);
}).then(res => 2 * res)
.then(res => console.log(res))
// 链式调用 输出 200
const p2 = new Promise((resolve, reject) => {
resolve(100);
}).then(res => new Promise((resolve, reject) => reject(3 * res)))
.catch(res => console.log(res))
// 链式调用 输出300
// 捕获 回调函数中的错误
const p3 = new Promise((resolve, reject) => {
resolve('执行成功');
}).then(res => {
throw new Error("执行回调发生错误")
}).catch(err=>{
console.log(err);
})
结论
-
then方法本身会返回一个新Promise对象。 -
如果回调函数返回值是Promise对象,则新Promise对象状态由该Promise对象决定。 -
如果回调函数返回值不是Promise对象,则新Promise对象状态为成功。 -
Promise会处理回调函数中的错误。
实现
then(onFulfilled, onRejected) {
...
var thenPromise = new MyPromise((resolve, reject) => {
const resolvePromise = cb => {
try {
const x = cb(this.PromiseResult)
if(x === thenPromise){
// 自己等待自己完成,循环等待:在then回调中返回了then的返回值。
reject(new TypeError('Chaining cycle detected for promise'));
}
if (x instanceof MyPromise) {
// 如果返回值是Promise对象,则新Promise状态由该Promise决定。
x.then(resolve, reject);
} else {
// 非Promise就直接成功
resolve(x);
}
} catch (err) {
// 处理报错
reject(err);
}
}
if (this.PromiseState === 'fulfilled') {
// 如果当前为成功状态,执行第一个回调
resolvePromise(onFulfilled);
} else if (this.PromiseState === 'rejected') {
// 如果当前为失败状态,执行第二个回调
resolvePromise(onRejected);
} else if (this.PromiseState === 'pending') {
// 如果状态为待定状态,暂时保存两个回调
this.onFulfilledCallback = resolvePromise(onFulfilled);
this.onRejectedCallback = resolvePromise(onRejected);
}
})
// 返回这个包装的Promise
return thenPromise;
}
5. 调用时序
输入/输出
setTimeout(()=>{console.log(0)},0);
const p = new Promise((resolve, reject) => {
console.log(1);
resolve()
}).then(() => console.log(2))
console.log(3)
// 输出顺序是 1 3 2 0
结论
-
即使Promise的状态被立刻更新为fulfilled,回调函数也不会被立刻执行。 -
回调函数是在事件循环的微任务中执行的。 -
因为回调本来就是异步的,放在微任务执行可以让后面的同步代码尽快被执行。
实现
const resolvePromise = cb => {
setTimeout(() => {
// 执行回调...
})
}
6. 应用场景
1. 封装异步回调
异步回调存在 Callback Hell
和缺少错误处理
等问题,使用Promise对已存在的异步回调进行封装。
// 下面是一个典型的超时回调,由于timeoutCallback是异步执行的,所以无法捕获回调中的错误。
try{
setTimeout(()=>timeoutCallback("3 seconds passed"), 3000);
}catch(err){
// 这里是无法捕获到 timeoutCallback错误的。
console.log(err);
}
// Promise可以将异步操作和回调分开,当异步操作完成后触发回调时,会自动在回调函数加上try catch。
const wait = ms => new Promise(
resolve => setTimeout(resolve, ms)
);
wait(3000).then(
() => timeoutCallback("3 seconds passed")
).catch(err=>{
console.log(err)
});
2. 执行多个异步操作
示例1:并行执行多个异步任务,并等待结果
// 并行多个异步操作,然后等所有操作完成后进入下一步操作
Promise.all([promise1, promise2, promise3])
.then(([result1, result2, result3]) => {
// next step
});
.catch(err){
// 当任意一个操作reject时,进入catch,并返回对应reject信息。
}
示例2:串联执行多个异步任务
// 按顺序执行多个异步操作(链式调用)
[promise1, promise2, promise3].reduce((pre, cur) => pre.then(()=>{return cur}), Promise.resolve())
.then(result3 => {
/* use result3 */
}).catch(err=>{
// 当任意一个操作reject时,进入catch,并返回对应reject信息。
});
// 在es2017中,也可以使用async/await对上面代码进行优化
let result;
try{
for (const pro of [promise1, promise1, promise1]) {
result = await pro(result);
}
}catch(err){
result = err;
}
示例3:控制异步任务的最大并发数。
class limitCountTaskExec{
constructor(maxCount){
this.maxCount = maxCount;
this.count = 0;
this.paddingTasks = [];
}
run(task){
if(task instanceof Array){
task.forEach(t=>{
this.run(t)
});
}else{
if(this.count != this.maxCount){
this.count++;
task().finally(()=>{
this.count--;
this.paddingTasks.length!=0 && this.run(this.paddingTasks.shift());
})
}else{
this.paddingTasks.push(task);
}
}
}
}
function request(data) {
return new Promise((resolve,reject) => {
setTimeout(() => {
reject('请求失败')
}, 1000)
})
}
let taskExec = new limitCountTaskExec(3);
for(let i=0; i<10; ++i){
taskExec.run(request);
}
3. 转移内部状态控制权
将 Promise 的 Resolve、Reject 的句柄传递到迭代函数中,来控制 Promise 的内部状态转化
示例1:请求失败时,最多重试3次。
function request(data) {
return new Promise((resolve,reject) => {
setTimeout(() => {
reject('请求失败')
}, 1000)
})
}
// Promise实现
function getLimiteTimeRequestPromise(ajaxRequest){
return function(){
let args = arguments;
let self = this;
return new Promise((resolve,reject)=>{
let count = 3;
function run(){
ajaxRequest.apply(self,args).then(data=>{
resolve(data);
}).catch(err=>{
console.log('请求失败,重试')
--count==0 ? reject(err) : run();
});
}
run();
});
}
}
// async实现
function getLimiteTimeRequestAsync(ajaxRequest){
return async function(){
let args = arguments;
let self = this;
let count = 3;
while(count--){
try{
let data = await ajaxRequest.apply(self,args);
return Promise.resolve(data);
}catch(err){
console.log('请求失败,重试')
if(0 == count){
return Promise.reject(err);
}
}
}
}
}
let limitRequest = getLimiteTimeRequestPromise(request);
limitRequest().then(data=>{
console.log(data)
}).catch(err=>{
console.log(err)
})
7. 不足
Promise解决了异步回调的Callback Hell问题。但不足之处是:
-
代码不够简洁
:需要通过then()注册回调函数。 -
链式调用时 无法断点
。
三. Generator
Generator可以控制函数的执行流程(暂停/执行),让用同步的方式来实现异步编程
成为了可能。
1. 控制函数执行流程
Generator对象
是由generator function
返回的一个可迭代对象。通过调用Generator对象的next()方法可以让generator function内部执行到下一个标有yield
关键字的位置。
通常一个函数被调用时,直到函数执行完毕后才把控制权转移给调用处代码。而Generator对象可以控制函数内部的执行流程(暂停和继续执行)
,例如:
function* gen(data){
let data1 = yield data+1; // 第一个断点
let data2 = yield data1+1; // 第二个断点
return data2+1;
}
let g = gen(1); // generator函数返回Generator对象
let data1 = g.next(); // 执行到gen函数第一个yield位置处:{value: 2, done: false}
let data2 = g.next(data1.value); // 执行到gen函数第二个yield位置处:{value: 3, done: false}
g.next(data2.value); // gen函数最后返回:{value: 4, done: true}
注:
-
next()的入参 会传给上一个yield语句=号左边的变量。 -
next()返回的对象包含value 和 done两个属性。value是当前yield表达式的值,done表示genenrator function是否执行完毕。
2. 异步编程转同步
Generator实现用同步的方式来实现异步编程
的思路是:控制函数在有异步操作的地方暂停,等待其完成并返回结果后,再继续执行。
function request(data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data * 2)
}, 1000)
})
}
function* gen(data){
let data1 = yield request(data);
let data2 = yield request(data1);
return request(data2);
}
let g = gen(1);
g.next().value.then(data1=>{
console.log(data1);
g.next(data1).value.then(data2=>{
console.log(data2);
});
});
上面的代码需要手动调用next()来执行下一个异步操作,下面我们通过一个高阶函数进行封装简化:
-
入参是generator function。返回值是一个返回Promise对象的函数。 -
函数内部自动执行next(),并判断是否执行完毕。
function generator2asyncFn(gen){
return function(){
// 可通过asyncFn给gen传参
let g = gen.apply(this,arguments);
return new Promise((resolve,reject)=>{
function go(data){
let nextResult;
try{
nextResult = g.next(data);
}catch(err){
return reject(err);
}
let {value,done} = nextResult;
if(done){
return resolve(value);
}else{
Promise.resolve(value).then(data=>{
return go(data);
}).catch(err=>{
return reject(err);
});
}
}
go();
});
}
}
通过generator2asyncFn高阶函数生成的asyncFn函数,可以自动执行next()并判断是否执行完毕,让generator function变得更加实用了。
function request(data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data * 2)
}, 1000)
})
}
function* gen(data){
let data1 = yield request(data);
let data2 = yield request(data1);
return data2;
}
let asyncFn = generator2asyncFn(gen);
asyncFn().then(res => console.log(res))
四. async/await
async/await其实是一个异步编程的语法糖,上面我们结合Promise和Generator实现的generator2asyncFn函数生成了一个async函数。
示例代码:
function request(data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data * 2)
}, 1000)
})
}
async function requests(data){
let data1 = await request(data);
let data2 = await request(data1);
return data2;
}
注:
-
await
操作符用于等待一个Promise
对象:返回Promise对象resolve值,非Promise对象返回表达式的值(区别于yield)。 -
await等待的Promise对象reject时,会抛出错误,可以被await外部catch。 -
async函数返回一个Promise对象。
五. 总结
Promise VS 异步回调
1. Callback Hell
Promise的链式调用
解决了Callback Hell
问题。
// 异步回调 实现串联执行多个异步操作
function request(data,callback){
setTimeout(()=>{
callback(data * 2);
},1000);
}
request(1,(data1)=>{
console.log(data1);
request(data1,(data2)=>{
console.log(data2)
});
})
// Promise 实现串联执行多个异步操作
request(1).then(data1=>{
console.log(data1)
return request(data1);
}).then(data2=>{
console.log(data2);
})
2. 错误处理
Promise对象会自动捕获处理器函数(Promise构造函数的入参)
和回调函数中的同步错误
。
注:什么是同步错误和异步错误?
// try/catch无法捕获异步执行的函数中的错误。
try{
setTimeout(()=>timeoutCallback("3 seconds passed"), 3000);
}catch(err){
// 这里是无法捕获到 timeoutCallback错误的。
console.log(err);
}
// 与之相反的就是同步错误
try{
throw new Error('发生错误');
} catch(err){
console.log(err); // 打印 Error: 发生错误
}
Promise在执行处理器函数
和回调函数
时都做了错误处理(详见上文)。
// 捕获处理器函数中的错误
const p1 = new Promise((resolve, reject) => {
throw new Error('处理器函数发生错误');
}).then(res => console.log('resolve=',res), err => console.log('reject=',err))
// 打印:reject= Error: 处理器函数发生错误
// 捕获回调函数中的错误
const p2 = new Promise((resolve, reject) => {
resolve('执行成功');
}).then(res => {
throw new Error('回调函数发生错误');
}).catch(err=>{
console.log('catch=',err);
})
// 打印:catch= Error: 回调函数发生错误
Promise VS async/await
1. 异步转同步
-
async/await 代码更简洁
:同步等待异步操作返回结果,不需要Promise.then()注册回调。
例如,连续发出两个请求,并且把第一个请求的结果作为第二个请求的参数。
function request(data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data * 2)
}, 1000)
})
}
request(1).then(data1=>{
console.log(data1)
return request(data1);
}).then(data2=>{
console.log(data2);
})
async function requests(data){
let data1 = await request(data);
let data2 = await request(data1);
return data2;
}
2. 异步流程控制
async/await解决了Promsie链式调用无法断点
的问题。
示例:连续发出两个请求,根据第一个请求的结果来确定是否发第二个请求。
function request(data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data * 2)
}, 1000)
})
}
async function requests(data){
let data1 = await request(data);
if(0 == data1){
return 0;
}
let data2 = await request(data1);
return data2;
}
参考文献
mdn_promise
mdn_async