
JS 中异步处理方案
本章介绍三种异步处理方案:
- 回调函数(callback)
- promise
- async/await
回调函数(callback)
回调函数应该属于最简单粗暴的一种方式,主要表现为在异步函数中将一个函数进行参数传入,当异步执行完成之后执行该函数
话不多说,上代码:
// 有三个任务 console.log(1)console.log(2)console.log(3)
// 过 5s 执行任务1,任务1 执行完后,再过 5s 执行任务2.....
window.setTimeout(function(){
console.log(1)
window.setTimeout(function(){
console.log(2)
window.setTimeout(function(){
console.log(3)
},5000)
},5000)
},5000)
看出这种方式的缺点了吗?没错,试想,如果再多几个异步函数,代码整体的维护性,可读性都变的极差,如果出了 bug,修复的排查过程也变的极为困难,这个便是所谓的 回调函数地狱。
promise
promise 简单的说就是一个容器,里面保存着某个未来才会结束的时间(通常是一个异步操作)的结果。从语法上说,promise 就是一个对象,从它可以获取异步操作的消息。promise提供统一的API,各种异步操作都可以用同样的方法处理。
如何理解:
- 没有异步就不需要promise
- promise 本身不是异步,只是我们去编写异步代码的一种方式
promise有所谓的 4 3 2 1
4 大术语
一定要结合异步操作来理解
既然是异步,这个操作需要有个等待的过程,从操作开始,到获取结果,有一个过程的
- 解决(fulfill)指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但在后世的 promise 实现多以 resolve 来指代之
- 拒绝(reject)指一个 promise 失败时进行的一系列操作
- 终值(eventual value)所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)
- 据因(reason)也就是拒绝原因,指在 promise 被拒绝时传递给拒绝回调的值
3 种状态
在异步操作中,当操作发出时,需要处于等待状态
当操作完成时,就有相应的结果,结果有两种:
- 成功了
- 失败了
一共是 3 种状态,如下:
- 等待态(Pending (也叫进行态)
- 执行态(Fulfilled)(也叫成功态)
- 拒绝态(Rejected) (也叫失败态)
针对每一种状态,有一些规范:
等待态(Pending)
处于等待态时,promise 需满足以下条件:
- 可以迁移至执行态或拒绝态
执行态(Fulfilled)
处于执行态时,promise 需满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的终值
拒绝态(Rejected)
处于拒绝态时,promise 需满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的据因
2 种事件
针对 3 种状态,只有如下两种转换方向:
- pending → fulfilled
- pendeing → rejected
在状态转换的时候,就会触发事件:
- 如果是pending → fulfiied,就会触发onFulFilled事件
- 如果是pendeing → rejected,就会触发onRejected事件
在调用 resolve 方法或者 reject 方法的时候,就一定会触发事件
需要注册 onFulFilled 事件 和 onRejected 事件
针对事件的注册,Promise 对象提供了 then 方法,如下:
promise.then(onFulFilled,onRejected)
针对 onFulFilled,会自动提供一个参数,作为终值(value)
针对 onRejected,会自动提供一个参数,作为据因(reason)
1 个对象
promise
注:只有异步操作的结果,可以决定当前是哪一种状态,任何其他的操作都无法改变这个状态
简单来讲,就还是 promise 中有着三种状态 pending,fulfilled,rejected。在代码中我们可以控制状态的变更
new Promise(function(resolve,reject){
console.log("pending");
console.log("pending");
resolve();
reject();
})
创建一个 Promise 对象需要传入一个函数,函数的参数是 resolve 和 reject,在函数内部调用时,就分别代表状态由 pending=>fulfilled(成功),pending=>rejected(失败)
一旦 promise 状态发生变化之后,之后状态就不会再变了。比如:调用 resolve 之后,状态就变为 fulfilled,之后再调用 reject,状态也不会变化
在创建 promise 对象,只需要根据需求,转换状态即可。无非就是调用两个函数:
- resolve,传递 value
- reject,传递 reason
Promise 对象在创建之后会立刻执行,因此一般的做法是使用一个函数进行包装,然后 return 一个 promise 对象
function betray(){
return new Promise(function(resolve,reject){
...// 异步操作
})
}
在使用时可以通过 promise 对象的内置方法 then 进行调用,then 有两个函数参数,分别表示 promise 对象中调用 resolve 和 reject 时执行的函数
function betray(){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve();
},1000)
})
}
betray().then(function(){
... // 对应 resolve 时执行的逻辑
},function(){
... // 对应 reject 时执行的逻辑
})
也可以用 catch 来执行失败态
catch 方法,用于注册 onRejected 回调
在这里要明白两件事情:
- catch 其实是 then 的简写,then(null,callback)
- then 方法调用之后,仍然返回的是 promise 对象,所以可以链式调用
使用如下:
betary().then(res=>... // 对应 resolve 时执行的逻辑).catch(err=>...//对应 reject 时执行的逻辑)
可以使用多个 then 来实现链式调用,then 的函数参数中会默认返回 promise 对象
betray().then(function(){
... // 对应 resolve 时执行的逻辑
},function(){
... // 对应 reject 时执行的逻辑
})
.then(function(){
... // 上一个 then 返回的 promise 对象对应 resolve 状态时执行的逻辑
},function(){
... // 上一个 then 返回的 promise 对象对应 reject 状态时执行的逻辑
})
使用 promise 来解决回调地狱的做法就是使用 then 的链式调用
function fnA(){
return new Promise(resolve=>{
... // 异步操作中 resolve
})
}
function fnB(){
return new Promise(resolve=>{
... // 异步操作中 resolve
})
}
function fnC(){
return new Promise(resolve=>{
... // 异步操作中 resolve
})
}
fnA()
.then(()=>{
return fnB()
})
.then(()=>{
return fnC()
})
特点是:
- then 方法通常是表示异步操作成功时的回调,也可以用 catch 方法表示异步操作失败时的回调
- 在调用的时候 then 在前后,catch 在后
- then 方法可以调用多次,前一个 then 的返回值,会作为后一个 then 的参数
- 支持链式调用
async/await
async、await 是什么?
async 顾名思义是 “异步” 的意思,async 用于声明一个函数是异步的。而 await 从字面意思上是 “等待” 的意思,就是用于等待异步完成。并且 await 只能在 async 函数中使用
通常 async、await 都是跟随 Promise 一起使用的。为什么这么说呢?因为 async 返回的都是一个 Promise 对象同时 async 适用于任何类型的函数上。这样 await 得到的就是一个 Promise 对象(如果不是 Promise 对象的话那 async 返回的是什么 就是什么);
await 得到 Promise 对象之后就等待 Promise 接下来的 resolve 或者 reject。
async、await 解决了什么?
传统的回调地狱式写法:
getData(a=>{
getMoreData(a,b=>{
getMoreData(b,c=>{
console.log(c)
});
});
});
// 不行了,再多写要迷了
Promise 改进后的写法:
getData()
.then(a=>getMoreData(a))
.then(b=>getMoreData(b))
.then(c=>getMoreData(c))
async/await 改进后:
(async()=>{
const a = await getData;
const b = await.getMoreData(a);
const c = await.getMoreData(b);
const d = await.getMoreData(c);
})();
async、await 写法
先来看看同步写法:
console.log(1);
setTimeout(function () {
console.log(2);
}, 1000);
console.log(3);
输出结果:
1
3
2
可以看到输出的顺序并不是我们代码中所写的那样,下面来看下 async、await 是如何解决这个问题的
(async function () {
console.log(1);
await new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(2);
resolve();
}, 1000);
});
console.log(3);
}())
输出结果:
1
2
3
可以看到这种写法的输出已经符合了我们的预期
async 的定义:
- async 函数会返回一个 Promise 对象
- 如果 async 函数中是 return 一个值,这个值就是 Promise 对象中 resolve 的值
- 如果 async 函数中是 throw 一个值,这个值就是 Promise 对象中 reject 的值
await 的定义:
- await 只能在 async 里面
- await 后面要跟一个 promise 对象
常规的 promise 对象会被 js 先暂存到 eventloop(事件队列)中,因为 js 是单线程执行的,等执行栈空了之后,才会将事件队列中的事件取出放入执行栈中执行
上述代码中先是将整段代码改造成了一个 async(async 可以用于任何函数)函数,然后又将 setTimeOut 改造成了一个 Promise 对象
使用第三方 Promise 库
下面简单介绍一下第三方的 Promise 库
对开发中使用 promise 进行小结:
- 没有异步,就不需要 promise
- 不使用 promise,其实也是可以解决异步编程的问题。使用 promise,会使异步的编码变得更加优雅,功能会更强
- 在进行 promise 编程的使用,有如下两个场景:
- 直接使用别人封装好的 promise 对象,比如 fetch、axios
- 需要自己封装 promise 对象
注意:axios 和 fetch 必须使用 promise 方式,如:
针对自己封装 promise 对象,又可以有如下两种方式:
- 自己封装
- 可以使用第三方的 promise 库
比如,针对第三方的 promise 库,有两个知名的库:
- bluebird
- q.js
可以利用 bluebird 和 q.js 快速的生成 promise 对象。
以bluebird 为例,在服务端演示其用法。
第一步:安装
第二步:使用