美文网首页
JS基础:异步编程之Promise、async-await

JS基础:异步编程之Promise、async-await

作者: 意一ineyee | 来源:发表于2019-08-28 11:06 被阅读0次

    目录

    一. Promise
     1. 为什么要使用Promise
     2. Promise是什么
     3. 如何使用Promise
     4.fetchAsyncStorage使用示例
    二. async-await

    一. Promise


    1. 为什么要使用Promise

    关于事件循环、线程、队列、同步任务、异步任务,这里就不展开了,简单说下它们在JS里的情况。

    为了程序执行的简单,JS被设计为单线程的,也就是说JS里的所有任务都是在主线程里执行的,下一个任务必须得等上一个任务执行完毕才能执行,这也就是同步任务。但是如果一个任务的执行时间可能很长(如一个任务里包含了网络请求、数据库读写等IO操作),它就会阻塞主线程,导致后面的任务也无法执行,不过让后面的任务等也不是不行,如果是因为某个任务计算量大而导致CPU忙不过来,那这个等就是不可避免的,也是有效的,但很多情况的等却是任务中IO操作的部分出结果很慢,导致我们一直拿不到结果,CPU就只能在那闲着干等,等到结果后再继续执行该任务。于是JS的设计者就设计,CPU完全可以不管某个任务中IO操作的部分,当某个任务执行IO操作的时候,就挂起这个任务,并把这个任务放到一个专门的队列中去,让CPU继续执行后面的任务,等IO操作返回了结果,再把这个任务从队列里拿出来放到主线程中继续执行下去,于是这种任务就成了异步任务,它不会阻塞主线程,而这种一遍一遍不停地检查异步任务是否该继续执行的机制就是JS里面的事件循环机制(Event Loop)。

    一个异步任务的通常写法都是:IO操作 + 回调函数。IO操作为一种耗时操作,回调函数用来指定耗时操作结束后接下来该任务要干什么。JS里设计如果一个异步任务没有回调函数,那么它在执行IO操作被挂起后,就不会把它放入任务队列中,那么当IO操作返回结果后,它也就不会再次进入主线程继续执行了,因为它没有用回调函数指定下一步要干什么。但如果异步任务指定了回调函数,那么当异步任务重新进入主线程时,就是执行对应的回调函数。

    下面我们举例子来看看异步任务的编写。

    假定f1要做个异步任务,f2f1的回调函数。

    function f1(callback) {
        // f1任务的耗时代码
        
        // f1任务的耗时代码执行完后,执行回调函数
        callback();
    }
    
    f1(f2);
    

    使用回调函数法来实现异步任务的优点是简单和容易理解,但是却可能出现下面这样的使用情况。

    function async(arg, callback) {
        console.log('参数为 ' + arg +' , 1秒后返回结果');
        setTimeout(function () { callback(arg * 2); }, 1000);
    }
    
    function final(value) {
        console.log('完成: ', value);
    }
    
    async(1, function (value) {
        async(2, function (value) {
            async(3, function (value) {
                async(4, function (value) {
                    async(5, function (value) {
                        async(6, final);
                    });
                });
            });
        });
    });
    
    
    // 参数为 1 , 1秒后返回结果
    // 参数为 2 , 1秒后返回结果
    // 参数为 3 , 1秒后返回结果
    // 参数为 4 , 1秒后返回结果
    // 参数为 5 , 1秒后返回结果
    // 参数为 6 , 1秒后返回结果
    // 完成:  12
    

    如果像上面这样,异步任务的回调函数又是一个异步任务,那回调函数就会一直嵌套下去,此时代码的结构就有点乱了,我们也无法从代码中清晰地阅读出每个异步任务的耗时任务完成后,接下来要做什么。

    Promise的出现,就是为了解决异步任务的回调函数可能过于臃肿和不易阅读的问题,下面我们来详细看看它。

    2. Promise是什么

    Promise的主要用途就是通过thencatch等方法来给异步任务设置回调,代替掉原来回调函数的那种实现方案,从而使得整个异步任务的流程更清晰,代码更易读。例如上面的例子,使用Promise后如下。

    (new Promise(stpe1))
        .then(step2)
        .then(step3)
        .then(step4);
    

    Promise是一个对象,也是一个构造函数。Promise构造函数接受一个函数f1作为参数,f1里面是异步任务的代码,返回的p1就是一个Promise对象。下面一小节我们会做详细的介绍。

    var p1 = new Promise(f1);
    

    Promise对象有三种状态。

    • 异步操作进行中(pending
    • 异步操作成功(fulfilled
    • 异步操作失败(rejected

    这三种状态之间的转变只有两种可能,而且一旦状态发生变化,就凝固了,不会再发生变化。

    • 异步操作进行中 ——> 异步操作成功
    • 异步操作进行中 ——> 异步操作失败

    因此,Promise对象的最终状态只有两种。

    • 异步操作成功
    • 异步操作失败

    注意:

    fulfilledrejected两种状态合在一起又可以称为resolved状态(已定型),但是为了行文方便,本篇后面的resolved状态统一只指fulfilled状态,不包含rejected状态。

    3. 如何使用Promise

    • 第一步:使用Promise构造函数,通过固定的格式来包裹异步任务,并将异步任务的执行结果或错误传递出去

    有了Promise之后,如果我们想给某个异步任务添加回调函数,就不是编写一个普通的函数,在内部做异步任务并执行回调函数了,而是直接使用Promise构造函数,通过固定的格式来包裹异步任务,并将异步任务的执行结果或错误传递出去。

    const promise = new Promise(function (resolve, reject) {
        // some code...
    
        if (/* 异步任务执行成功 */){
            resolve(value);
        } else { /* 异步任务执行失败 */
            reject(error);
        }
    });
    

    上面代码中,Promise构造函数接收一个函数作为参数。该参数函数的两个参数分别是而且必须是resolvereject,它们俩是JS提供的系统函数,不需要我们自己部署,我们只要这么固定地写就可以了;该参数函数的执行体就是要执行的异步任务,并通过resolve(value)reject(error)固定的写法,将异步任务的执行结果或错误传递出去,执行体会在Promise对象创建之后立即执行。

    通过以上的固定写法,我们知道resolve函数会在异步操作成功时触发,并将异步操作的结果作为参数传递出去,这个函数的执行会把Promise对象的状态从pending变为resolvedreject函数会在在异步操作失败时触发,并将异步操作的错误作为参数传递出去,这个函数的执行会把Promise对象的状态从pending变为rejected

    • 第二步:使用Promise的then方法和catch方法,为异步任务添加回调函数

    在第一步中,我们并没有直接为异步任务添加回调函数,而仅仅是通过resolve(value)reject(error)把异步任务的结果或错误传递出来了,现在我们来为异步任务添加回调函数。

    Promise对象生成之后,我们可以通过它的then方法,分别指定它变为resolved状态(即异步任务执行成功)和rejected状态(即异步任务执行失败)后的回调函数。

    promise.then(function (value) {
        // success
    }, function (error) {
        // failure
    });
    

    then方法可以接受两个回调函数作为参数。第一个回调函数会在异步任务执行成功调用,第一步传出来的value就能在这里接收到;第二个回调函数会在异步任务执行失败调用,第一步时传出来的error就能在这里接收到。其中第二个函数是可选的,不一定要提供。

    同时then方法执行后的返回值又是一个新的Promise对象(注意不是原来那个Promise对象了),因此可以对then方法采用链式写法,这时上一个then方法参数函数的执行结果,会自动传递给下一个then方法的参数函数作为参数。

    promise.then(function (异步任务的执行结果) {
        // ...
        return 结果1;
    }).then(function (结果1) {
        // ...
        return 结果2;
    }).then(function (结果2) {
        // ...
    });
    

    除了then方法之外,Promise还有一个catch方法,它其实是.then(null, rejection).then(undefined, rejection)的别名,可以专门用来指定Promise对象变为rejected状态(即异步任务执行失败)的回调函数。

    promise.then(function () {
        // ...
        return 结果1;
    }).then(function (结果1) {
        // ...
        return 结果2;
    }).then(function (结果2) {
        // ...
    }).catch(function (error) {
        // ...
    });
    

    catch方法可以捕捉它上面所有then方法的错误,使用catch方法捕捉错误要比使用then方法既捕捉成功也捕捉的代码看起来清晰明了。因此我们推荐,使用then方法提供异步任务成功的回调,而使用catch方法提供异步任务失败的回调。

    4.fetchAsyncStorage使用示例

    // ProjectRequest.js
    
    /**
     * RN提供的fetch方法,是异步的,它本身就会返回一个Promise对象。因为这里我们对它进行了封装使用,所以外面又包了一层Promise,来给fetch这个异步任务提供回调,这样外界才能拿到它的结果。
     *
     * @param url
     * @param params
     * @returns {Promise<any> | Promise}
     */
    static post(url, params) {
        return new Promise((resolve, reject) => {
            fetch(url, {
                method: 'POST',
                headers: {
                    Accept: 'application/json',
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(params)
            })
                .then(response => {
                    if (response.ok) {
                        // 请求到的response其实是一个Response对象,它是一个很原始的数据格式,我们不能直接使用,先获取它的JSON字符串文本格式
                        return response.text();
                    } else {
                        throw new Error('网络请求失败!');
                    }
                })
                .then(responseText => {
    
                    // 然后把JSON字符串序列化为JS对象
                    const responseJSObj = JSON.parse(responseText);
    
                    // 把请求成功的数据传递出去
                    resolve(responseJSObj);
                })
                .catch((error) => {
                    // 把请求失败的信息传递出去
                    reject(error);
                })
        })
    }
    
    // ThemeDao.js
    
    /**
     * 读取主题色
     * RN提供的AsyncStorage,它的读取和写入操作都是是异步的,只通过回调函数的方式来告诉我们结果。因为这里我们对它进行了封装使用,所以外面又包了一层Promise,来给AsyncStorage这个异步任务提供回调,这样外界才能拿到它的结果。
     *
     * @returns {Promise<any> | Promise}
     */
    static getThemeColor() {
        return new Promise((resolve, reject) => {
            AsyncStorage.getItem(THEME_COLOR, (error, value) => {
                if (error) {
                    reject(error);
                } else {
                    if (!value) {// 数据库中还没有存主题色
                        // 那就搞个默认的主题色
                        value = AllThemeColor.Default;
    
                        // 存起来
                        this.saveThemeColor(value);
                    }
    
                    // 传出去
                    resolve(value);
                }
            });
        });
    }
    

    二. async-await


    async-await的主要作用就是用来将一个异步任务变成同步的。

    // 存储的数据为:{'hey': '你好'}
    
    _read() {
        console.log(1);
        AsyncStorage.getItem('hey', (error, value) => {
            if (error) {
                console.log('读取数据出错:', error);
            } else {
                console.log(2);
                console.log(value);
                console.log(3);
            }
        });
        console.log(4);
    }
    

    比如上面这串代码,是从数据库里读取一些数据,因为AsyncStorage.getItem这个操作是异步的,所以会依次输出1、4、2、你好、3。

    但有时候,我们希望确确实实从数据库读到了数据再执行后面的操作,而不是把操作放到异步操作的回调里执行,此时就可以用async-await将一个异步任务变成同步的。

    async _read() {
        console.log(1);
        await AsyncStorage.getItem('hey', (error, value) => {
            if (error) {
                console.log('读取数据出错:', error);
            } else {
                console.log(2);
                console.log(value);
                console.log(3);
            }
        });
        console.log(4);
    }
    

    这样,代码在执行到await的地方就会阻塞住,直到它后面的异步操作执行完毕,才会执行后面的语句,async只是个标识符,没什么实际的意义。这样会依次输出1、2、你好、3、4。

    相关文章

      网友评论

          本文标题:JS基础:异步编程之Promise、async-await

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