vlambda博客
学习文章列表

如何用同步的方式来实现异步编程?

 文章介绍了异步回调PromiseGeneratorasync/await四种异步编程方案,通过分析它们的演进过程,我们可以看到Promise解决了异步回调的Callback Hell问题;Generator的出现让用同步的方式来实现异步编程成为了可能,async/await则是结合了Promise和Generator优势的语法糖。在开发中,我们应该根据实际情况,选择最合适的方案。

关键词:异步回调PromiseGeneratorasync/awaitCallback 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);
})

输出:

image-20220102102219777

结论:

  1. 刚创建的Promise对象处于 pending状态。
  2. 当异步操作完成后, 调用resolve/reject变更状态为fulfilled/rejected
  3. 当处理器函数出错( 同步错误)时,变更状态为rejected。
image-20220102193706740

实现:

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);
})

结论

  1. then方法本身会返回一个新Promise对象。
  2. 如果回调函数返回值是Promise对象,则新Promise对象状态由该Promise对象决定。
  3. 如果回调函数返回值不是Promise对象,则新Promise对象状态为成功。
  4. 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

结论

  1. 即使Promise的状态被立刻更新为fulfilled,回调函数也不会被立刻执行。
  2. 回调函数是在事件循环的微任务中执行的。
  3. 因为回调本来就是异步的,放在微任务执行可以让后面的同步代码尽快被执行。

实现

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对象可以控制函数内部的执行流程(暂停和继续执行),例如:

functiongen(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)
  })
}

functiongen(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)
  })
}

functiongen(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