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

目录

一. 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 | 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 | 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)