JavaScript中的异步编程

一、回调函数

因为在JS中函数是一等公民,所以它可以像其他变量一样作为参数进行传递。例如下方这段登录时的业务逻辑代码:

let key, token, userId;

$.ajax({
    type: 'get',
    url: 'http://localhost:3000/apiKey',
    success: function (data) {
        key = data;
        
        $.ajax({
            type: 'get',
            url: 'http://localhost:3000/getToken',
            data: {
                key: key
            },
            success: function (data) {
                token = data.token;
                userId = data.userId;
                
                $.ajax({
                    type: 'get',
                    url: 'http://localhost:3000/getData',
                    data: {
                        token: token,
                        userId: userId
                    },
                    success: function (data) {
                        console.log('业务数据:', data);
                    },
                    error: function (err) {
                        console.log(err);
                    }
                });
            },
            error: function (err) {
                console.log(err);
            }
        });
    },
    error: function (err) {
        console.log(err);
    }
});

整段代码充满了回调嵌套,代码不仅在纵向扩展,横向也在扩展,这就是我们常说的回调地狱(Callback Hell)

二、Promise

ES6的Promise也好,jQuery的Promise也好,不同的库有不同的实现,但是大家遵循的都是同一套规范,所以,Promise并不指特定的某个实现,它是一种规范,是一套处理JavaScript异步的机制。

用Promise的方式重构上面那段代码:

let getKeyPromise = function () {
    return new Promise((resolve, reject) => {
        $.ajax({
            type: 'get',
            url: 'http://localhost:3000/apiKey',
            success: function (data) {
                let key = data;
                resolve(key);   
            },
            error: function (err) {
                console.log(err);
            }
        });

    });
}

let getTokenPromise = function (key) {
    return new Promise((resolve, reject) => {
        $.ajax({
            type: 'get',
            url: 'http://localhost:3000/getToken',
            data: {
                key: key
            },
            success: function (data) {
               resolve(data);
            },
            error: function (err) {
                console.log(err);
            }
        });
    });
}

let getDataPromise = function(data) {
    let token = data.token;
    let userId = data.userId;
    
    return new Promise((resolve,  reject) => {
        $.ajax({
            type: 'get',
            url: 'http://localhost:3000/getData',
            data: {
                token: token,
                userId: userId
            },
            success: function (data) {
                resolve(data);
            },
            error: function (err) {
                console.log(err);
            }
        });
    });
}

getKeyPromise()
    .then(key => {
        return getTokenPromise(key);
    })
    .then(data => {
        return getDataPromise(data);
    })
    .then(data => {
        console.log("业务数据" + data);
    })
    .catch(err => {
        console,log(err);
    });

Promise去除了横向扩展,无论有再多的业务依赖,通过多个then(…)来获取数据,再一点就是逻辑性更明显,层级比较清晰,Promise在一定程度上解决了回调函数的书写结构问题,但回调函数依然在主流程上存在,只不过都放到了then(…)里面。

首先明确一点,Promise可以保证以下情况,引用自JavaScript | MDN:

在JavaScript事件队列的当前运行完成之前,回调函数永远不会被调用
通过 .then 形式添加的回调函数,甚至都在异步操作完成之后才被添加的函数,都会被调用
通过多次调用 .then,可以添加多个回调函数,它们会按照插入顺序并且独立运行

三、生成器函数Generator

一种顺序、看似同步的异步流程控制表达风格,这就是ES6中的生成器(Gererator)。
Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

yield表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

可迭代协议和迭代器协议
了解Generator之前,必须先了解ES6新增的两个协议:可迭代协议和迭代器协议。

可迭代协议
可迭代协议运行JavaScript对象去定义或定制它们的迭代行为,例如(定义)在一个for…of结构中什么值可以被循环(得到)。以下内置类型都是内置的可迭代对象并且有默认的迭代行为:

Array
Map
Set
String
TypedArray
函数的Arguments对象
NodeList对象
注意,Object不符合可迭代协议。

为了变成可迭代对象,一个对象必须实现@@iterator方法,意思是这个对象(或者它原型链prototype chain上的某个对象)必须有一个名字是Symbol.iterator的属性:

属性
[Symbol.iterator] 返回一个对象的无参函数,被返回对象符合迭代器协议

迭代器协议
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。

属性
next 返回一个对象的无参函数,被返回对象拥有两个属性

其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束,遍历结束时value为undefined,done为true。

再次重构上面打代码:

function getKey () {
    $.ajax({
        type: 'get',
        url: 'http://localhost:3000/apiKey',
        success: function (data) {
            key = data;
            it.next(key);
        }
        error: function (err) {
            console.log(err);
        }
    });
}

function getToken (key) {
    $.ajax({
        type: 'get',
        url: 'http://localhost:3000/getToken',
        data: {
            key: key
        },
        success: function (data) {
            loginData = data;
            it.next(loginData);
        }
        error: function (err) {
            console.log(err);
        }
    });
}

function getData (loginData) {
    $.ajax({
        type: 'get',
        url: 'http://localhost:3000/getData',
        data: {
            token: loginData.token,
            userId: loginData.userId
        },
        success: function (busiData) {
            it.next(busiData);
        }
        error: function (err) {
            console.log(err);
        }
    });
}



function *main () {
    let key = yield getKey();
    let LoginData = yield getToken(key);
    let busiData = yield getData(loginData);
    console.log('业务数据:', busiData);
}

// 生成迭代器实例
var it = main();

// 运行第一步
it.next();
console.log('不影响主线程执行');

四、Async/Await

上面我们介绍了Promise和Generator,把这两者结合起来,就是Async/Await。

Generator的缺点是还需要我们手动控制next()执行,使用Async/Await的时候,只要await后面跟着一个Promise,它会自动等到Promise决议以后的返回值,resolve(…)或者reject(…)都可以

用Async/Await的方式改写上面的代码:

let getKeyPromise = function () {
    return new Promsie(function (resolve, reject) {
        $.ajax({
            type: 'get',
            url: 'http://localhost:3000/apiKey',
            success: function (data) {
               let key = data;
               resolve(key);         
            },
            error: function (err) {
                reject(err);
            }
        });
    });
};

let getTokenPromise = function (key) {
    return new Promsie(function (resolve, reject) {
        $.ajax({
            type: 'get',
            url: 'http://localhost:3000/getToken',
            data: {
                key: key
            },
            success: function (data) {
                resolve(data);         
            },
            error: function (err) {
                reject(err);
            }
        });
    });
};

let getDataPromise = function (data) {
    let token = data.token;
    let userId = data.userId;
    
    return new Promsie(function (resolve, reject) {
        $.ajax({
            type: 'get',
            url: 'http://localhost:3000/getData',
            data: {
                token: token,
                userId: userId
            },
            success: function (data) {
                resolve(data);         
            },
            error: function (err) {
                reject(err);
            }
        });
    });
};

async function main () {
    let key = await getKeyPromise();
    let loginData = await getTokenPromise(key);
    let busiData = await getDataPromise(loginData);
    
    console.log('业务数据:', busiData);
}

main();

console.log('不影响主线程执行');

Async/Await是Generator和Promise的组合,完全解决了基于回调的异步流程存在的两个问题,可能是现在最好的JavaScript处理异步的方式了。

总结

JavaScript异步编程的发展历程阶段:
1. 第一个阶段
回调函数,但会导致两个问题:

  • 缺乏顺序性:回调地狱导致的调试困难,和大脑的思维方式不符
  • 缺乏可信任性:控制反转导致的一系列信任问题
    2. 第二个阶段
    Promise,Promise是基于PromiseA+规范的实现,它很好的解决了控制反转导致的信任问题,将代码执行的主动权重新拿了回来。
    3. 第三个阶段
    生成器函数Generator,使用Generator,可以让我们用同步的方式来书写代码,解决了顺序性的问题,但是需要手动去控制next(…),将回调成功返回的数据送回JavaScript主流程中。
    4. 第四个阶段
    Async/Await,Async/Await结合了Promise和Generator,在await后面跟一个Promise,它会自动等待Promise的决议值,解决了Generator需要手动控制next(…)执行的问题,真正实现了用同步的方式书写异步代码。

你可能感兴趣的:(JavaScript中的异步编程)