本文目录:
- 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方法
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中抽离出来,避免低层次的逻辑影响了高层次的逻辑;这也应该是所有的高度抽象化代码中必要的一个环节,不同逻辑层次的代码混杂在一起最终会像回调地狱一样让自己和读自己代码的人陷入混乱。