美文网首页
JavaScript中的异步编程

JavaScript中的异步编程

作者: ChooAcc | 来源:发表于2020-10-11 21:18 被阅读0次

    一、回调函数

    因为在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中的异步编程

          本文链接:https://www.haomeiwen.com/subject/exqusktx.html