JS异步之callback、promise、async+await简介

1,226 阅读2分钟

所谓同步执行,就是从前往后执行,一句执行完了执行下一句。

而异步执行,则不会等待异步代码执行完,直接执行后面的代码;等异步代码执行完返回结果之后再进行处理。

比如一些耗时操作会使用异步执行的方式,以提高执行效率,最常见的莫过于Ajax网络请求。

以jQuery Ajax为例,假设有这样的需求:

从地址A获取数据a,从地址B获取数据b,然后把数据a和b提交到接口C

ES5:使用callback实现

使用最原始的方式直接来写,大概要写成这样子:

// 显示加载提示
$.showLoading();

// 发起ajax请求
$.ajax({
    url: 'https://example.com/api/a',
    type: 'get',
    success: function(res1){
        $.ajax({
            url: 'https://example.com/api/b',
            type: 'get',
            success: function(res2){
                $.ajax({
                    url: 'https://example.com/api/c',
                    type: 'post',
                    success: function(res3){
                        $.hideLoading();
                        $.toast('请求成功');
                    },
                    error: function(res3){
                        $.alert('请求失败: ' + res3);
                    }
                });
            },
            error: function(res2){
                $.alert('请求失败: ' + res2);
                $.hideLoading();
            }
        });
    },
    error: function(res1){
        $.alert('请求失败: ' + res1);
        $.hideLoading();
    }
});

这就产生了回调嵌套的问题,写出来的代码既难看又不方便维护。

要解决这个问题,可以写个函数包装一下,把相似的逻辑分离出来执行,比如:

// 请求过程包装函数
function Process(){
    this.requests = []; // 保存请求方法
    this.count = 0; // 记录执行到第几个
    this.result = []; // 保存返回结果
    this.onComplete = null; // 请求完成回调
    this.onSuccess = null; // 请求成功回调
    this.onError = null; // 请求出错回调
    // 执行请求的方法
    this.continue = function(){
        if(this.count < this.requests.length){
            var fn = this.requests[i];
            fn();
            this.count += 1;
        } else {
            this.onComplete(this.result);
        }
    }
}

// 创建对象
var p = new Process();

// 将请求方法放入
p.requests.push(function(){
    $.ajax({
        url: 'https://example.com/api/a',
        type: 'get',
        success: function(res){
            p.onSuccess(res);
        },
        error: function(res){
            p.onError(res);
        }
    });
});
p.requests.push(function(){
    $.ajax({
        url: 'https://example.com/api/b',
        type: 'get',
        success: function(res){
            p.onSuccess(res);
        },
        error: function(res){
            p.onError(res);
        }
    });
});
// 当请求成功
p.onSuccess = function(res){
    // 存储返回结果
    p.result.push(res);
    // 执行下一个
    p.continue();
};
// 当请求失败
p.onError = function(res){
    $.alert('请求失败: ' + res);
    $.hideLoading();
};
// 当请求完成
p.onComplete = function(result){
    $.ajax({
        url: 'https://example.com/api/c',
        type: 'post',
        data: {
            a: result[0],
            b: result[1]
        },
        success: function(res){
            $.hideLoading();
            $.toast('请求成功');
        },
        error: function(res){
            p.onError(res);
        }
    });
};

// 显示加载提示
$.showLoading();
// 执行请求
p.continue();

这样通过一个包装函数统一进行处理,从而避免了回调嵌套的问题。

根据不同的业务逻辑,不同的编程习惯,这类函数实现的方式也各不相同。那么有没有一种方式,可以适用所有异步调用呢?

ES6:Promise

Promise就给出了这样一套规范,可以使用Promise来处理异步操作。

Promise规范详细的内容和实现比较复杂,需要另写一篇文章,这里就只介绍一下Promise的用法。

如何用Promise执行jQuery Ajax请求:

// 首先创建一个Promise对象实例,传入一个执行函数
// 执行成功时调用resolve方法,传入返回结果
// 执行失败时调用reject方法,传入错误原因
let p = Promise(function(resolve, reject){
    $.ajax({
        url: 'https://example.com/api/a',
        type: 'get',
        success: function(res){
            resolve(res);
        },
        error: function(res){
            reject(res);
        }
    });
});
// promise有一个then方法,在then方法中执行回调
// 第一个是成功回调,第二个是失败回调
p.then(function(data){
    console.log('请求成功', data);
}, function(err){
    console.error('请求失败', err);
});

也可以只传入成功回调,在后面用catch方法来捕获错误,这样可以统一处理错误:

// 使用catch方法捕获错误
p.then(function(data){
    console.log('请求成功', data);
}).catch(function(err){
    console.error('请求失败', err);
});

Promise的链式调用:

// 在回调中返回新的Promise,可以在下一个then中执行回调,实现链式调用
p.then(function(data){
    console.log('A请求成功', data);
    return new Promise(function(resolve, reject){
        $.ajax({
            url: 'https://example.com/api/b',
            type: 'get',
            success: function(res){
                resolve(res);
            },
            error: function(res){
                reject(res);
            }
        });
    });
}).then(function(data){
    console.log('B请求成功', data);
}).catch(function(err){
    console.error('请求失败', err);
});

前面需求的完整Promise方式实现:

// 用于返回jQuery Ajax的Promise方法,从而简化代码
let promisifyAjaxRequest = function(url, data){
    let method = data ? 'post' : 'get';
    return new Promise(function(resolve, reject){
        $.ajax({
            url: url,
            type: method,
            data: data,
            success: function(res){
                resolve(res);
            },
            error: function(res){
                reject(res);
            }
        });
    });
};

// 显示加载提示
$.showLoading();
// 用于保存请求数据结果
let result = [];
promisifyAjaxRequest('https://example.com/api/a').then(function(data){
    result.push(data);
    return promisifyAjaxRequest('https://example.com/api/b');
}).then(function(data){
    result.push(data);
    return promisifyAjaxRequest('https://example.com/api/c', {
        a: result[0],
        b: result[1]
    });
}).then(function(data){
    $.hideLoading();
    $.toast('请求成功');
}).catch(function(err){
    $.alert('请求失败: ' + err);
    $.hideLoading();
});

可以看到使用Promise之后的代码变得更加简洁和统一。

但是Promise也有不足之处,就是使用起来不是特别直观。

ES7:async+await

在ES7中,提供了async和await关键字,被称为异步的终极解决方案。

这个方案其实也是基于Promise实现的,具体细节就先不展开,还是重点来看使用方式。

使用async+await来实现前面需求:

// 因为基于Promise实现,所以同样需要返回Promise
let promisifyAjaxRequest = function(url, data){
    let method = data ? 'post' : 'get';
    return new Promise(function(resolve, reject){
        $.ajax({
            url: url,
            type: method,
            data: data,
            success: function(res){
                resolve(res);
            },
            error: function(res){
                reject(res);
            }
        });
    });
};

// 函数定义前面加上async关键字
async function ajaxRequests(){
    // promise方法前面加上await关键字,可以接收返回结果
    let a = await promisifyAjaxRequest('https://example.com/api/a');
    let b = await promisifyAjaxRequest('https://example.com/api/b');
    let data = await promisifyAjaxRequest('https://example.com/api/c', {
        a: a,
        b: b
    });
    return data;
}

// 显示加载提示
$.showLoading();
// 执行请求
// 用法同Promsie
ajaxRequets().then(function(data){
    $.hideLoading();
    $.toast('请求成功');
}).catch(function(err){
    $.alert('请求失败: ' + err);
    $.hideLoading();
});

可以看到,async+await的写法更加直观,也更像同步的写法。