异步方法:callback、Promise、async/await
1. Promise
1.1 Promise 解决了什么问题
为什么说Promise解决了地狱回调的问题?我们把地狱回调的代码改写成这样,这样的代码组织形式没有了层层嵌套的问题,咋一看,甚至还比Promise的要简洁。但它的每一处依然容易受到“回调地狱”的影响。为什么呢?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 回调
function main () {
then(1)
}
function then1 (res) {
then2(2)
}
function then2 () {}4
// Promise
new Promise((resolve, reject) => {
resolve(1)
})
.then(res => {
return new Promise((resolve, reject) => {
resolve(2)
})
})
.then(res => { })
为了将第2,3,4步链接在一起使他们相继发生,回调需要我们将第2 步硬编码在第1 步中,将第3 步硬编码在第2 步中,将第4 步硬编码在第3 步中,如此继续。
Promise解决了什么问题:
提供一个标准化的异步管理流程
需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”。你只需要设置回调,等待承诺兑现,解决回调函数相互调用之间的标准(信任)问题。链式调用取代回调嵌套,处理流程更线性
过多的回调会导致代码的逻辑不连贯、不线性,不符合直觉。Promise封装异步代码,可以让处理流程变得线性,只需要关注输入和输出。指定回调函数更加灵活
promise之前:必须在启动异步任务前指定
promise:启动异步任务=> 返回promise对象=> 给promise 对象绑定回调函数(可以绑定多个,延时绑定)
1.1 Promise 实现原理
Promise实际还是使用回调函数。它为每一个异步任务创建一个Promise实例,维护一个观察者模式。通过.then
收集回调 -> 异步触发resolve -> resolve触发回调执行。
基本功能
- Promise接收一个
executor
,在new Promsie
的时候立刻执行 - 异步任务被放入宏任务队列,等待执行
then()
被执行,收集回调放入成功/失败回调队列- 异步任务执行成功调用
resolve
,执行失败调用reject
,成功/失败回调队列执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 一个极简的Promise
class MyPromise {
constructor(executor) {
this._resolveQueue = [] // then收集的执行成功的回调队列
// resolve等待被异步任务执行,resolve执行时,取出回调一次执行
let _resolve = (val) => {
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
// new Promise()时立即执行executor
executor(_resolve, _reject)
}
// then方法,接收一个成功的回调和一个失败的回调,并push进队列
then(resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn)
}
}
- Promise接收一个
链式调用
then
负责收集成功回调 和 失败回调then
返回的一定是一个新的Promise,保证每个promise都是独立的then
的回调需要拿到上一个then
的返回值- 当
then
里是一个promise的时候,如何将这个promise的值传递给下一个then
延迟机制
无论resolve
是被同步执行,还是异步执行,都放在setTimeout
中保证异步执行。保证回调函数可以延迟绑定状态机
Promise状态只能有Pending
,Fulfilled
,Rejected
,状态的变更是单向的,只能从Pending -> Fulfilled, Pending -> Rejected,状态变更不可逆
1.2 实现一个Promise
1 | //Promise/A+规定的三种状态 |
1.3 Promise API
Promise.prototype.catch()
设置Promise的失败回调1
2
3catch (rejectFn) {
return this.then(undefined, rejectFn)
}Promise.prototype.finally()
在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。1
2
3
4
5finally (callback) {
return this.then(
value => callback().then(())
)
}Promise.resolve()
返回一个给定值resolve后的promise对象1
2
3
4static resolve (value) {
if (value instanceof MyPromise) return value
return new MyPromise(resolve => resolve(value))
}Promise.reject()
返回一个带有拒绝原因的Promise对象1
2
3static reject (reason) {
return new MyPromise((resolve, reject) => reject(reason))
}Promise.all()
所有promise都完成(resolved)时1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21static all (promiseArr) {
let index = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
// Promise.resolve(p)用于处理传入只不为promise的情况
MyPromise.resolve(p).then(
val => {
index++
result[i] = val
if (index === promiseArr.length) {
resolve(result)
}
},
err => {
reject(err)
}
)
})
})
}Promise.race()
一旦某个promise resolve或reject,返回的promise就会resolve或reject1
2
3
4
5
6
7
8static race () {
return MyPromise((resolve, reject) => {
MyPromise.resolve(p).then(
value => { resolve(value )},
err => { reject(err) }
)
})
}
2. async/await
提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰,而且还支持 try-catch 来捕获异常,非常符合人的线性思维。
2.1 async/await解决了什么问题
- 过多的链式调用可读性依然不佳
- 流程控制不方便
异步任务a->b->c之间存在依赖关系,如果我们通过then链式调用来处理这些关系,可读性并不是很好,如果我们想控制某些条件下,b不往下执行到c,那也不是很方便控制。但是如果用async/await来实现这个场景,可读性和流程控制都会方便不少。1
2
3
4
5
6
7
8
9
10
11
12
13Promise.resolve(a)
.then(b => {
// do something
})
.then(c => {
// do something
})
async () => {
const a = await Promise.resolve(a);
const b = await Promise.resolve(b);
const c = await Promise.resolve(c);
}。
2.1 Generator暂停恢复执行原理
async/await实际上是对Generator(生成器)的封装,是一个语法糖,并对Generator进行了改进。
Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态,但是只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
1 | function* helloWorldGenerator() { |
2.2 async/await实现原理
async函数对 Generator 函数的改进,体现在以下四点:
- 内置执行器。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。
- 更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
- 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。
1 | // 定义了一个promise,用来模拟异步请求,作用是传入参数++ |
2.3 async/await优点
- 做到了真正的串行同步写法
- 对条件语句和其他流程语句比较友好,可以直接写到判断条件里
1
2
3async function f () {
if (await a() === 2) {}
}
2.4 async/await缺点
- async await是有传染性的 —— 当一个函数变为async后,这意味着调用他的函数也需要是async。
无法处理promise返回的reject对象,
await g()
会直接报错,必须用try…catch捕获1
2
3
4
5
6
7
8
9
10
11
12
13function g () {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('no')
}, 2000)
})
}
async function f () {
const y = await g()
console.log(y)
}
f()await只能串行,做不到并行。await一定是阻塞的,甚至可以阻塞for循环。await做不到并行,不代表async不能并行。只要await不在同一个async函数里就可以并行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26Promise.all(ajax1(), ajax2())
function g () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('no')
}, 2000)
})
}
// 耗时20s才打印完
async function f () {
for (let var i = 0; i < 10; i++) {
const y = await g()
console.log(y)
}
}
// async实现并行
function f () {
[1,1,1].forEach(async () => {
const y = await g()
console.log(y)
})
}全局捕获错误必须用window.onerror,不像Promise可以专用window.addEventListener(‘unhandledrejection’, function),而window.onerror会捕获各种稀奇古怪的错误,造成系统浪费
- try..catch内部的变量无法传递个下一个try..catch
- 无法简单实现Promise的各种原生方法,如race()