持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天,点击查看活动详情
async 和 await
针对异步编程 , 我们学习过 Ajax 的回调形式 , promise 的链式调用形式
ajax 的回调模式
回调地狱:
这种回调模式,嵌套非常深,阅读性极差,代码使用非常不方便。所以产生了,阅读性更高,更加直观的,Promise 链式调用形式。
异步编程的终极方案 async/await
async 和 await 的基本使用形式
async 和 await 实际上就是让我们像写同步代码那样去完成异步操作
await 表示强制等待的意思,await 关键字的后面要跟一个Promise 对象,他总是等到该Promise 对象 resolve 成功之后执行,并且会返回resolve 的结果
async test () {
// await 总是会等到后面的 Promise 执行完 resolve
// async / await 就是让我们用同步的方法去写异步
const result = await new Promise(function (resolve , reject){
setTimeout(function(){
resolve(5)
},5000)
alert(result)
})
}
上面这段代码在等待五秒之后,会alert (5)
async 和 await 必须成对出现
由于await 的强制等待,所以必须要求使用 await 的函数必须使用async 标记 , async 表示该函数就是一个异步函数,不会阻塞其他执行逻辑的执行,
而async 必须在 使用await 函数的父级函数的前面标记
async test () {
// await 总是会等到后面的 Promise 执行完 resolve
// async / await 就是让我们用同步的方法去写异步
const result = await new Promise(function (resolve , reject){
setTimeout(function(){
resolve(5)
},5000)
alert(result)// 异步代码不会阻塞其他任务的执行
})
}
test1(){
this.test()
alert('同步代码test1')
}
观察上方代码运行结果,会发现,先alert('同步代码test1'),再alert(5),即异步代码不会阻塞其他任务的执行。
而如果有需要,在alert(5) 之后,再执行一段代码,可以将代码改造:
async test () {
// await 总是会等到后面的 Promise 执行完 resolve
// async / await 就是让我们用同步的方法去写异步
const result = await new Promise(function (resolve , reject){
setTimeout(function(){
resolve(5)
},5000)
alert(result)// 异步代码不会阻塞其他任务的执行
})
}
async test1(){
await this.test()
alert('被改造的代码test1')
}
这样的代码运行结果是 alert(5) ,然后 alert('被改造的代码test1'),这也充分的说明了 被 async 标记的函数返回的实际上也是 Promise 对象
如何捕获 async/await?
那么如果 Promise 异常了 , 怎么处理?
Promise 可以通过 catch 捕获,async / await 捕获异常要通过 try/catch
async getCatch () {
try{
await new Promise (function(resolve,reject){
reject(new Error('失败'))
})
alert('这个弹出不会被执行,因为上面的 Promise reject了一个错误实例对象,而没有resolve ,所以会产生报错,这里需要使用try/catch 捕获错误')
}catch (error) {
alert(error)// 这里的错误会被catch 捕获,最后弹出'失败'
}
}
promise
promise 是什么?
在控制台打印promise === console.dir(promise)
可见 promise 是一个构造函数,在 promise 内部,拥有 all,reject,resolve 等等方法,在原型上有then ,catch 等等方法,那么使用 Promise new 出来的对象就拥有 then ,catch 方法
new 一个 promise 实例对象
Promise 的构造函数接收一个参数,是函数,并且传入两个参数:resolve , reject ,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数,当然这里使用“成功”和“失败”来描述并不准确,按照标准来讲,resolve 是将 Promise 的状态置为fullfiled ,reject 是将 Promise 的状态置为 rejected 。
观察上面的代码,在向Promise 传入的函数参数中包含了一个异步操作,即 setTimeout , 2秒后,输出“执行完成”,并且调用resolve 方法。
运行代码,会在2秒后输出“执行完成”,但是!上面的代码中只是new了一个Promise对象,但是并没有调用它,而传进去的函数参数就已经执行了,所以,在使用Promise 的时候一般是包在一个函数中,在需要使用的时候再运行这个函数,比如:
到这里,有两个问题:
- 包装这样一个函数有什么作用呢?
- 异步操作中的 resolve('随便什么数据') 有什么作用呢?
Promise 对象的 then , catch 方法
接着上面的代码,在包装好的函数最后,会return 出Promise 对象,也就是说执行这个函数,会获得一个Promise 对象, 在Promise 对象上我们已知拥有 then , catch 方法,观察下方代码:
在 runAsync() 的返回上直接调用 then 方法,then 接收一个参数,是函数,并且会拿到在 runAsync 中调用 resolve 时传的参数,而运行这段代码会在2秒后输出执行完成,紧接着输出语句:“随便什么数据”
这时可得出,then 里面的函数的功能与回调函数功能相同,能够在 runAsync 这个异步任务执行完成之后被执行,这就是 Promise 的作用,简单讲就是能把原来的回调写法分离出来,在异步操作执行完成后,用链式调用的方式执行回调函数。
而在简单回调时,将回调函数封装,再传入runAsync 是一样的,如:
// 执行 runAsync (function(data){console.log(data)}),会在2秒后打印'执行完成' '随便什么数据'
与使用Promise 的效果是一样的,那么使用Promise的优势在哪呢?
在需要使用多层回调时,使用Promise
这里的多层回调指,当callback 也是异步操作时,执行完成后也需要有相应的回调函数,那么使用封装回调函数的方法在这时就非常的麻烦,但是使用Promise ,就可以在then 方法中继续写Promise 对象并且返回,然后继续调用 then 来进行回调操作,结构清晰便于理解,也更加省事。
链式操作的用法(then 的用法)
从表面上看,Promise 只是能够简化层层回调的方法 , 而实质上 , Promise 的精髓是“状态” , 用维护状态,传递状态的方式来使得回调函数能够及时调用,它比传递 callback 函数要简单 , 灵活的多, 所以使用 Promise 的正确场景是这样的:
这样能够按照顺序,每隔两秒输出每个异步回调中的内容,在runAsync 中传给 resolve 的数据,能在接下来的 then 方法中拿到,运行结果如下:
而 runAsync1 , runAsync2,runAsync 这三个函数的定义如下,他们都包含了一个 Promise 的构造函数:
数据传入流程如下:
在 then 方法中 , 也可以直接 return 数据 而不是 Promise 对象,在后续的 then 中就可以接收到数据了,比如把上述的代码改成这样:
输出就会变成这样:
数据流程如下:
reject 的用法
到这里,对于Promise 是什么,已经做了一个最基本的介绍,接下来来看看 ES6 的Promise 还有哪些功能,目前为止还只介绍了 resolve 还没有用 reject ,那么reject 是做什么的呢?
在之前的例子中,都只有“执行成功” 的回调,还没有“执行失败”的情况,reject 的作用就是把 Promise 的状态设置为 rejected ,这样在 使用 then 时就能捕捉到,然后再执行“失败”情况的回调,看下方的代码:
function getNumber(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
var num = Math.ceil(Math.random()*10); //生成1-10的随机数
if(num<=5){
resolve(num);
}
else{
reject('数字太大了');
}
}, 2000);
});
return p;
}
getNumber()
.then(
function(data){
console.log('resolved');
console.log(data);
},
function(reason){
console.log('rejected');
console.log(reason);
}
);
getNumber 函数用来异步获取一个数字,2秒后执行完成,如果数字小于等于5,则认为函数执行“成功”,调用 resolve 修改 Promise 的状态为 resolved , 否则认为函数执行“失败”,调用reject 并传递一个参数,作为失败的原因
运行 getNumber 并在 then 中传两个参数, then 方法 可以接受两个参数,第一个对应 resolve 的回调,第二个对应 reject 的回调,所以可以分别拿到 getNumber 传递的数据,多次运行上面这段代码,可以得到两种结果:
// 第一种
resolved
1(这里可能为 1,2,3,4,5)
// 第二种
rejected
数字太大了
catch 的用法
我们知道 Promise 对象除了 then 方法 ,还有一个 catch 方法,它是做什么用的呢?其实它和 then 的第二个参数一样,用来指定 reject 的回调,用法是这样的:
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
效果和下载 then 的第二个参数里面一样,不过它还有另外一个作用:在执行resolve 的回调(也就是上面 then 中的第一个参数)时,如果抛出异常了(代码出错),那么并不会报错卡死 js ,而是会进到 这个 catch 方法中,观察下方代码:
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
在 resolve 的回调中,在 console.log(somedata) 时,这个变量somedata 是没有被定义的,如果不用 Promise ,代码运行到这里就会直接在控制台报错,不再往下运行,但是这里不会,会得到这样的结果:
resolved
4
rejected
ReferenceError:somedata is not defined(...)
也就是说,这个错误进入到 catch 方法里面了,而且 then 方法把错误原因传到了 reason 参数中,即便是有错误代码也不会报错,这与 try/catch 语句有相同的功能。
all 的用法
Promise 的 all 方法 提供并执行了 异步操作的能力,并在所有的异步操作执行完成后才执行回调,下面仍旧使用定义好的 runAsync1,runAsync2,runAsync3 这三个函数,看下面的代码:
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
用 Promise.all 来执行,all 接收一个数组参数,里面的值最终都算返回 Promise 对象,这样,三个异步操作并行执行,等到他们都执行完后才会进到 then 里面,那么三个异步操作返回的数据哪里去了呢?都在 then 里面, all 会把所有异步操作的结果放进一个数组中传给 then ,就是上面的 results ,所以代码的输出结果就是:
异步任务1执行完成
异步任务2执行完成
异步任务3执行完成
["随便什么数据1","随便什么数据2","随便什么数据3"]
有了 all ,就可以并执行多个异步操作,并在一个回调中处理所有的返回数据,使用场景:在一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如 图片, flash 以及各种静态文件,所有的都加载完后,再进行页面的初始化。
race 的用法
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,我们把上面runAsync1的延时改为1秒来看一下:
Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
这三个异步操作同样是并行执行的。不同的是,1秒后runAsync1已经执行完了,此时then里面的就执行了。结果是这样的:
异步任务1执行完成
随便什么数据1
异步任务2执行完成
异步任务3执行完成
观察上方打印结果可知,在 then 里面的回调开始执行时, runAsync2()和 runAsync3()并没有停止,仍旧在执行,于是再过1s后 ,输出了它们结束的标志。
race 的使用场景
例如:使用 race 给某个异步请求设置超时时间,并在超时后执行响应的操作,代码如下:
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = 'xxxxxx';
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
上面代码中定义 requestImg 函数会异步请求一张图片,这里把地址写为'xxxxx' ,所以肯定是无法成功请求到的。 timeout 函数是一个延时 5 秒的 异步操作,把这两个返回 Promise 对象的函数放进 race ,于是race 就会判断这两个函数的执行速度,如果5秒后图片还未请求成功,则定时器触发,抛出定时器内的reject (),进入 catch(),捕捉到“图片请求超时”的报错信息,运行结果如下:
总结
-
ES6 Promise 的内容就这些吗?
- 常用的基本就这些 :then , reject , catch , all , race
-
其他的常见的 done ,finally ,success ,fail 等等方法,这些与Promise 的关系是什么?
- 这些方法并不在Promise 标准中,是开发者自行实现的语法糖