美文网首页我爱编程
JS基础系列(异步编程解决方案)

JS基础系列(异步编程解决方案)

作者: NSO | 来源:发表于2018-04-13 22:51 被阅读28次

    大家在js编程中肯定遇到过异步问题,实践中大家在异步问题上也不断提出新的解决方案,这里就梳理一下异步解决方案。

    回调函数

    回调函数是最原始的解决方案,逻辑上很容易理解。看个例子:

    function f1(value, callback) {
        console.log('f1');
        setTimeout(
            () => {
                value = setValue('f2');
                callback.call(this, value);
            }
            , 1000
        );
    }
    function f2(value){
        console.log(value);
    }
    function setValue(value) {
        return value;
    }
    
    let value;
    f1(value, f2);
    
    结果.png
    这里的f2需要f1延时1s完成value的重新赋值后再打印value,比较简单的处理就是把f2作为f1的回调函数。

    这是历史最悠久的方案

    • 优点:兼容性好
    • 缺点:当回调函数不断嵌套时,代码会横向发展,形成“意大利面”(Italian noodles)代码

    Promise对象

    随着历史的发展,在前端社区的某些库中有人提出了Promise方案,后来这个方案也被写入官方标准。本文不再谈各种前端库中Promise的实现,只谈目前ES规范中的Promise
    几个要点:

    • Promise是一个原生对象,我们使用Promise对象是利用它的API。
    • Promise对象的异步操作有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。回调函数只在fulfilled(已成功)和rejected(已失败)状态下触发。
    • Promise对象的实例可以调用then方法指定不同状态的回调函数(或catch方法注册rejected(已失败)状态的回调函数)

    使用Promise对象重新实现上文的例子:

    let value;
    function f1() {
        console.log('f1');
        const promise = new Promise((resolve, reject) => {
            setTimeout(
                () => {
                    let value = setValue('f2');
                    if (!!value) {
                        resolve(value);
                    } else {
                        reject('error');
                    }
                }
                , 1000);
        });
        return promise;
    }
    function f2(value){
        console.log(value);
    }
    function setValue(value) {
        return value;
    }
    
    f1()
        .then(
            f2,
            (error) => console.log(error)
        );
    
    结果.png
    可以看到函数f1返回了一个Promise对象的实例,这个实例就具有.then/.catch等方法注册回调函数。
    再看一个使用Promise实现的AJAX封装
    function fetchRemote(url, method='GET', param=null) {
        const promise = new Promise(function(resolve, reject) {
            let xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.setRequestHeader("Accept", "application/json");
            xhr.onreadystatechange = function() {
                if (xhr.readyState !== 4){
                    return;
                }
                if (xhr.status === 200 || xhr.status === 304) {
                    resolve(xhr.response);
                } else {
                    reject(xhr.status);
                }
            };
            xhr.send(param);
        });
        return promise;
    }
    
    // 调用
    fetchRemote(url)
        .then(
            (response) => {
                todo... 
            }
        )
        .catch(
            (error) => {
                todo...
            }
        )
    

    这里提一个小细节: 请注意XMLHttpRequest对象的实例具有实例方法onreadystatechange(),实际上这是一个注册回调函数的api,每一次XMLHttpRequest实例对象的readyState/status被改变时,回调函数都会被异步触发

    此外,Promise对象也提供了一些新API适应不同场景,例如.race/.all

        // Promise.race() 适用于等待多个promise第一个完成的场景
        Promise.race(task1, task2, task3...).then().catch();
        
        // Promise.all() 适用于等待多个promise同时完成的场景
        Promise.all(task1, task2, task3...).then().catch();
    

    Generator函数

    学习过Generator函数的同学应该知道,Generator函数最大的特点就是不会顺序执行到底,遇到yield语句时会暂停,直到调用next方法才会继续执行。显然,这种特性很适合异步场景。
    还是实现最初的小例子:

    const setValue = value => value;
    let value;
    function* f1() {
        console.log('f1');
        yield new Promise((resolve, reject) => {
            setTimeout(() => {
                value = setValue('f2');
                if (!!value) {
                    resolve(value);
                } else {
                    reject('error');
                }
            }, 1000);
        });
    }
    function f2 (value) {
        console.log(value);
    }
    
    let g = f1();
    g.next().value
        .then(f2)
        .catch((error) => {console.error(error);});
    
    结果.png

    再看一个远程请求的例子:

    function* fetchData(url) {
        const data = yield fetch(url);
        yield handler(data);
    } 
    
    const g = fetchData('http://url/api');
    g.next();
    g.next();
    

    可以看到,通过使用Generator函数的属性,异步逻辑变得非常简单,只需要在异步等待的函数处“暂停”即可。这种语法在redux-saga的库中有很好的应用。

    async函数

    async函数顾名思义,就是为解决异步场景而生的。
    async函数返回的是一个Promise对象,所以async函数返回的结果可以调用.then()/.catch()方法,本质上和promise对象用法类似,只是有了语法糖书写更简便。
    还是这个熟悉的老例子:

    const setValue = value => value;
    let value;
    function f1() {
        console.log('f1');
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                value = setValue(null);
                if (!!value) {
                    resolve(value);
                } else {
                    reject('error');
                }
            }, 1000);
        });
    }
    function f2 (value) {
        console.log(value);
    }
    
    async function fn() {
        return await f1();
    }
    fn()
        .then(
            (value) => {console.log(value);}
        )
        .catch(
            (error) => console.error(error)
        );
    
    结果.png
    简单解释一下,asyncawait需要互相配合,await只能在async函数中使用,await等的是promise对象,如果await得到的不是promise对象则会把普通对象直接作为promisevalue并更新状态为resolved
    可以很明显看出,一般情况下async函数不具有优势,它的优势在处理then链。
    看个例子:
    async function manycbs() {
        const arg1 = 'xxx';
        const arg2 = await step1(arg1);
        const arg3 = await step2(arg1, arg2);
        const result = await step3(arg1, arg2, arg3);
        console.log(result);
    }
    
    manycbs();
    

    这样就可以看出async函数可以把异步写得像同步一样。

    相关文章

      网友评论

        本文标题:JS基础系列(异步编程解决方案)

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