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<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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,693评论 0 5
  • 你不知道JS:异步 第三章:Promises 在第二章,我们指出了采用回调来表达异步和管理并发时的两种主要不足:缺...
    purple_force阅读 2,047评论 0 4
  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,698评论 1 56
  • 你不知道JS:异步 第三章:Promises 接上篇3-1 错误处理(Error Handling) 在异步编程中...
    purple_force阅读 1,384评论 0 2
  • 概述 Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(p...
    nucky_lee阅读 962评论 0 1