异步方案之Promise和Async/Await

本文目录:

  • 1.什么是异步
  • 2.什么是Promise
  • 3.实例练习:按顺序异步加载图片
  • 4.Promise相关方法
  • 5.异步方案优化之Async/Await
  • 6.async await与Promise并发性能优化

1.什么是异步

要理解异步,首先,从同步代码开始说

alert(1)
alert(2)

像上面的代码,执行顺序是从上到下,先后弹出1和2,这种代码叫做同步代码

alert(0)
setTimeout(function () {
    alert(1);
}, 2000);
setTimeout(function () {
    alert(2)
}, 1000);

alert(3)

上面代码的弹出顺序是 0 3 2 1 ,像这种不按从上到下依次执行的代码叫做异步代码,其实还有很多类似的异步代码,例如:ajax请求

ajax({
    type:'get',
    url: 'http://xxx.com/xxx',
    success: function(result){}
        console.log(111)
    })

异步回调嵌套问题:回调地狱

setTimeout(function () {
    alert(1)
    setTimeout(function () {
        alert(2)
        setTimeout(function () {
            alert(3)
        }, 10)
    }, 100)
}, 1000)

上面的代码可读性很差,让代码变得难以维护,针对这种情况,ES6提出了Promise。

2.什么是Promise

Promise是ES6中的异步编程解决方案,在代码中表现为一个对象,可以通过构造函数Promise来实例化,有了Promise对象,可以将异步操作以同步的流程表达出来,避免了回调地狱(回调函数层层嵌套)
直观的去看看Promise到底是什么
console.dir(Promise)
这样一看就很明白了,Promise是一个构造函数,它身上有几个方法,例如:reject、resolve、catch、all、race等方法就是我们常用的一些方法,还有then方法在它的原型上,也是非常常用的,后面我们会详细讲解这些方法。
既然是构造函数,那么我们就可以使用new来调用一下,简单的使用

let p = new Promise((resolve, reject) => {
       setTimeout(()=>{
           //代码执行完成
           console.log('代码执行完成');
           resolve()
       }, 1000)
   })

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),上面代码中传入的函数有两个参数,resolve和reject,这两个参数都是函数块,用于回调执行,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected,只有这两个结果可以去操作Promise的状态,其他任何操作都不能更改这个状态,这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。在初学阶段你可以简单的理解为resolve就是异步执行成功后被调用的函数,reject是异步执行失败后调用的函数
注意: 上面代码中我们只是去new Promise() 得到一个实例,但是发现异步代码中的语句在1秒后被执行了,也就是说只要new Promise(), 那么promise里面的函数就会被立即执行,这是非常重要的一个细节,我们应该做到需要的时候去执行,而不是不管什么情况都去执行,因此,我们通常把上面的代码包到一个函数中去,需要的时候,调用一下函数就可以了

function AsyncFn() {
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            //代码执行完成
            console.log('代码执行完成');
            resolve()
        }, 1000)
    });
    return p;
}

上面的代码在执行之后会返回一个promise对象,代码中return的主要意义是让我们接下来可以调用promise的prototype里面的then方法。
函数封装好后到底有什么用?在什么情况下用?resolve拿来做什么? 带着这些疑问,我们继续往下讲。
在Promise的原型上有一个叫做then的方法,它的作用是为 Promise 实例添加状态改变时的回调函数,我们首先来看看then方法的位置。


then的位置.png

下面我们来具体使用这个then方法

function AsyncFn() {
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            //代码执行完成
            console.log('代码执行完成');
            resolve()
        }, 1000)
    });
    return p;
}
AsyncFn().then(function () {
    alert('异步代码执行完成后,该我执行了')
})

注意:then里面的第一个函数是一个函数块,这个函数块被传给了promise里面的resolve()
then里面的第一个函数块本来的位置就是嵌套在promise函数里面的回调函数
代码写到这里,我们已经能看出Promise的作用了,它其实已经可以把原来回调函数函数写到异步代码里的这种写法改变了,它已经把回调函数函数分离出来了,在异步代码执行完成后,通过链式调用的方式来执行回调函数,如果仅仅是向上面的代码只执行一次回调函数可能看不出Promise带来的好处,下面我们来看更复杂的代码:

function AsyncFn1() {
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('异步代码1代码执行完成');
            resolve()
        }, 1000)
    });
    return p;
}
function AsyncFn2() {
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            //代码执行完成
            console.log('异步代码2执行完成');
            resolve()
        }, 3000)
    });
    return p;
}
function AsyncFn3() {
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            //代码执行完成
            console.log('异步代码3执行完成');
            resolve()
        }, 2000)
    });
    return p;
}

需求:AsyncFn3 是依赖于AsyncFn2的 AsyncFn2是依赖于AsyncFn1的,这就要求AsyncFn1执行完成后再执行AsyncFn2,AsyncFn2执行完成后执行AsyncFn3,这个时候怎么写?

 AsyncFn1().then(()=>{
        alert('异步代码1执行完成后,该我执行了');
        //上面代码执行完成后,返回一个Promise对象
        return AsyncFn2()
    }).then(()=>{
         alert('异步代码2执行完成后,该我执行了');
         return AsyncFn3()
    }).then(()=>{
        alert('异步代码3执行完成后,该我执行了');
    })

到底为止,Promise的作用已经差不多可以理解了,它是ES6中的异步解决方案,可以将异步的代码以同步的形式表现出来,避免回调函数函数嵌套
如果理解了resolve的话,那么理解reject就比较容易了,它是异步代码执行失败后执行的回调函数。reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调
then方法的第一参数是交给resolve执行,第二个方法是交给reject执行

let oBtn = document.getElementById('btn');
let oResult = document.getElementById('result');
oBtn.onclick = () => {
    AsyncGetData().then(() => {
        oResult.innerHTML = '执行成功,获取到了数据。。。'
    }, () => {
        oResult.innerHTML = '<span style="color: red">执行失败,没有获取到数据。。。</span>'
    })
};
function AsyncGetData() {
    let num = Math.random() * 20;
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (num > 10) {
                resolve();
            } else {
                reject();
            }
        })
    })
}

3.实例练习:按顺序异步加载图片

首先定义一个数组arr,里面存放着几张网络图片资源的链接
const arr = ['xxxx','xxxx','xxxx','xxxx'];
在页面中定义一个按钮,id为btn,点击这个按钮可以按顺序依次加载arr数组中的四张图片,并且将图片标签添加到页面body元素中
<button id="btn">点我加载图片</button>
首先获取到这个button元素
let oBtn = document.getElementById('btn');
如果按照传统的写法可以很轻松的实现,如下面的代码,但是这样写的缺点是图片的加载是无序的

oBtn.onclick = () => {
    for (let i = 0; i < arr.length; i++) {
        AsyncLoadImg(arr[i]).then(function (oResult) {
            document.body.appendChild(oResult);
        })
    }
}

promise写法如下,顺利实现需求:

function AsyncLoadImg(url) {
    let p = new Promise((resolve, reject) => {
        let oImg = new Image();
        oImg.src = url;
        oImg.onload = () => {
            //加载成功,触发此事件,写加载成功的业务逻辑
            resolve(oImg)
        };
        oImg.onerror = () => {
            //加载失败,触发此事件,手动抛出一个错误
            let error = new Error('图片加载失败');
            reject(error)
        }
    });
    return p;
}
oBtn.onclick = () => {
    //按顺序加载
    AsyncLoadImg(arr[0]).then((oResult) => {
        oResult.title = '图片1';
        document.body.appendChild(oResult);
        return AsyncLoadImg(arr[1])
    }(err) => {
        console.log(err)
    }).then((oResult) => {
        oResult.title = '图片2';
        document.body.appendChild(oResult);
        return AsyncLoadImg(arr[2])
    }, (err) => {
        console.log(err)
    }).then((oResult) => {
        oResult.title = '图片3';
        document.body.appendChild(oResult);
        return AsyncLoadImg(arr[3])
    }, (err) => {
        console.log(err)
    }).then((oResult) => {
        oResult.title = '图片4';
        document.body.appendChild(oResult);
    })
}

4.Promise相关方法

1.catch的用法
catch方法和then的第二个参数作用差不多,都是用来指定异步执行失败后的回调函数函数的,不过,它还有一个功能就是如果在resolve中抛出错误,不会阻塞执行,而是可以在catch中捕获到错误

AsyncGetData().then(() => {
    oResult.innerHTML = '执行成功,获取到了数据。。。'
    throw new Error('这里报错了')
}).catch((e) => {
    console.log(e)
})

2.all方法
all方法中传入一个数组,里面是多个Promise实例,只有当所有的Promise实例的状态变为fulfilled的时候,整体的状态才会变成fulfilled,这个时候每个Promise的实例返回的值会组成一个数组传给回调函数,如果整个数组中的Promise实例中有一个的状态是rejected,那么整体的状态都会是rejected,这个时候,第一个rejected实例的返回值会传给回调函数。
和刚才实现异步按顺序加载图片的案例一样,我们首先定义一个arr数组,里面存放的也是四个网络图片资源的地址。这次我们利用all方法对代码进行优化

Promise.all([AsyncLoadImg(arr[0]), AsyncLoadImg(arr[1]), AsyncLoadImg(arr[2]), AsyncLoadImg(arr[3])])
    .then((result) => {
        console.log(result)
        for (let i in result) {
            document.body.appendChild(result[i]);
        }
    })

如果执行成功的话,promise实例返回的值会放到result中,组成一个新的数组。
all方法通常适用于先加载资源,再执行操作的场景,例如:在练手案例贪吃蛇项目,首先要去加载地图、图片、以及声音等这些资源,等加载成功后再执行初始化
3.race方法
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数,数组中的元素谁用时最少,就以谁为准。
例如上面的异步加载图片案例中,我们可以给加载图片的时间设置一个时间限制,超过这个时间,就会中止加载,并且被catch捕捉
图片超时测试代码:

function timeOut() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('图片超时'));
        }, 1000)
    })
}

给第一个图片资源的加载加上时间限制:

oBtn.onclick = () => {
    let p = Promise.race([AsyncLoadImg(arr[0]), timeOut()])
        .then((result) => {
            document.body.appendChild(result)
        }).catch((err) => {
            console.log(err);
        })
}

5.异步方案优化之Async/Await

我们都知道使用Promise能很好地解决回调地狱的问题,但如果处理流程比较复杂的话,那么整段代码将充斥着then,语义化不明显,代码不能很好地表示执行流程,那有没有比Promise更优雅的异步方式呢?
假如有这样一个使用场景:需要先请求a链接,等返回信息之后,再请求b链接的另外一个资源。下面代码展示的是使用fetch来实现这样的需求,fetch被定义在window对象中,它返回的是一个Promise对象

fetch('https://blog.csdn.net/')
  .then(response => {
    console.log(response)
    return fetch('https://juejin.im/')
  })
  .then(response => {
    console.log(response)
  })
  .catch(error => {
    console.log(error)
  })

虽然上述代码可以实现这个需求,但语义化不明显,代码不能很好地表示执行流程。基于这个原因,ES8引入了async/await,这是JavaScript异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。

async function foo () {
  try {
    let response1 = await fetch('https://blog.csdn.net/')
    console.log(response1)
    let response2 = await fetch('https://juejin.im/')
    console.log(response2)
  } catch (err) {
    console.error(err)
  }
}
foo()

通过上面代码,你会发现整个异步处理的逻辑都是使用同步代码的方式来实现的,而且还支持try catch来捕获异常,这感觉就在写同步代码,所以是非常符合人的线性思维的。需要强调的是,await 不可以脱离 async 单独存在。

async function foo () {
  return '浪里行舟'
}
foo().then(val => {
  console.log(val) // 浪里行舟
})

上述代码,我们可以看到调用async 声明的foo 函数返回了一个Promise对象,等价于下面代码:

async function foo () {
  return Promise.resolve('浪里行舟')
}
foo().then(val => {
  console.log(val) // 浪里行舟
})

6.async await与Promise并发性能优化

有时候我们需要等待两个异步结果来执行接下来的操作,这个时候,如果使用promise或者async/await写出来的流程,会存在性能上的差异。
我们看一下这个例子的运行时间

function foo() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(10)
        }, 1000)
    })
}
function bar() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(20)
        }, 1500)
    })
}
async function test() {
    let start = new Date().getTime()
    let val1 = await foo()
    let val2 = await bar()
    let val  = val1 +val2
    let end  = new Date().getTime()
    let time = end - start
    console.log(val)
    console.log(time)
}
test()

输出结果:
30 =>val的结果
2502 =>test()运行时间
函数foo和bar写法,test函数我们换一种写法,再来看看运行时间:

async function test() {
    let start = new Date().getTime()
    let p1 = foo()
    let p2 = bar()
    let val1 = await p1
    let val2 = await p2
    let val  = val1 +val2
    let end  = new Date().getTime()
    let time = end - start
    console.log(val)
    console.log(time)
}
test()

输出结果:
30 =>val的结果
1501 =>test()运行时间
换一种写法,test()的运行时间少了一秒钟是不是?
这是因为第一种写法,必须等foo()运行结束才能运行bar(),所以所用的时间是两个异步Promise等待时间的和;
而第二种写法中,因为提前定义p1和p2,提前运行了这两个Promise,程序运行到await p1的时候两个Promsie都已经开始运行,也就是它们是并行的;
这样test()的运行时间主要就取决于用时更长的那个Promise而不是两者的相加。
或者我们也可以使用Promise.all()来实现,foo和bar函数的写法依旧不变

async function test() {
    let start = new Date().getTime()
    let vals = await Promise.all([foo(), bar()])
    let val  = vals[0] +vals[1]
    let end  = new Date().getTime()
    let time = end - start
    console.log(val)
    console.log(time)
}

输出结果:
30 =>val的结果
1501 =>test()运行时间
这种写法已经相当靠谱,但是,我们还是有优化的空间;
async/await函数不应该关心异步Promise的具体实现细节,await应该只关心最终得到的结果,这样为更加复杂的异步操作提供更加清晰的过程控制逻辑。

function getVal() {
    return Promise.all([foo, bar])
}
async function test() {
    let vals = await getVal()
    let val = vals[0] + vals[1]
    console.log(val)
}

应该有意识的把这种逻辑从async/await中抽离出来,避免低层次的逻辑影响了高层次的逻辑;这也应该是所有的高度抽象化代码中必要的一个环节,不同逻辑层次的代码混杂在一起最终会像回调地狱一样让自己和读自己代码的人陷入混乱。

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

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,705评论 1 56
  • 你不知道JS:异步 第三章:Promises 在第二章,我们指出了采用回调来表达异步和管理并发时的两种主要不足:缺...
    purple_force阅读 2,064评论 0 4
  • JavaScript里通常不建议阻塞主程序,尤其是一些代价比较昂贵的操作,如查找数据库,下载文件等操作,应该用异步...
    张歆琳阅读 2,755评论 0 12
  • 前言 本文旨在简单讲解一下javascript中的Promise对象的概念,特性与简单的使用方法。并在文末会附上一...
    _暮雨清秋_阅读 2,197评论 0 3
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,710评论 0 5