【Javascript基础】前端人必须要会的Promise和async/await

92 阅读12分钟

众所周知,js是单线程的,一些需要等待之后才能获取数据的操作,如果程序就这样挂起,用户的体验就会十分不友好。于是就诞生了异步处理的操作。 没有异步之前:

去蛋糕店买蛋糕,排队,每个顾客都需要等厨师做完之后才能付钱走人

有异步

去蛋糕店买蛋糕,排队,收银人员会给顾客一个小票,告诉你第几号,之后你可以先做其他事,等号到了过去取。

有了异步的操作,我们的效率会大大提升,代码也是这样。

早期的异步模式

在早期的javascript中,异步还不完善,当时只能使用回调函数的方式处理异步。

function double(value){
	setTimeout(console.log,0,value*2)
}
foo(2)//4

虽然setTimeout设置的延时是0,但是因为setTimeout的机制(微任务/宏任务),实际上执行console.log是一个异步的操作,这个代码逻辑并不复杂,我们增加场景,让回调函数式的异步出现的问题浮现出来。 假设setTimeout中的部分有返回值,如何把他的返回值在需要的地方使用?使用一个回调,将结果当作参数传过来。

function double(value,callback){
    setTimeout(()=>callback(value*2),0)
}

double(2,(val)=>{console.log(`double val is ${val}`)})

看上去还好,那再假设 callback的回调又依赖了另一个异步的返回值?

function double(value,callback){
    setTimeout(()=>callback(value*2),0)
}
const callback =(val)=>{
    double(val,callback2)
}
const callback2=(val)=>{
    console.log(`最终的结果是${y}`)
}
double(2,callback)

是不是看上去很绕了?这才两层嵌套,当然 还有错误处理没有考虑:

function double(value, successCallback,errCallback) {
  setTimeout(() => {
    try {
      successCallback(value * 2)
    } catch (error) {
      errCallback(error)
    }
  }, 0)
}
const callback = (val) => {
  double(val, callback2)
}
const callback2 = (val) => {
  console.log(`最终的结果是${val}`)
}
const errCallback=(err)=>{
  console.log(`err is ${err}`)
}
double(2, callback,errCallback)

随着逻辑的增加,这种写法显然会让人头疼,“回调地狱”实至名归

Promise

翻译成中文就是期约,promise这个名词很早都已经载javascript中流行起来了,但是直到es6才彻底完善,并开始广泛使用。

1. Promise基础

es6新增了Promise,它是一个类,通过new的方式创建,参数是一个executor(执行器)函数,我们看一个简单的例子

const p = new Promise(()=>{})
console.log(p)//Promise {<pending>}

可以看到 有一个pending,这个是promise的一个状态机,表示了promise的一个状态,pending表示现在它处于一个待定的状态,promise有三种状态

  1. pending:待定
  2. resovled/fulfilled:兑现
  3. rejected:拒绝

pending是一个初始的状态,在pending状态可以通过操作让其转为resolved或者rejected状态,状态机是无法再次更改的,也就是说,一个promise的状态,只能从pending->resovled或者pending->rejected。并且,该属性是私有的,外部无法访问到。

promise的成功、失败、实例

promise是一种对异步的抽象处理,它内部的三种状态也描述了一个异步操作不同时刻的状态。 比如,当我们向服务发送了一个请求,可以根据服务器返回的状态码是否是200-299来判断promise的状态是否是 resovled,如果不在200-299就把状态置为rejected。 当然除了状态之外,还有对数据的处理,发送请求也会有响应的,即使请求失败,也会有请求失败的原因,无论如何,成熟的异步必须能够接收到这个响应值或者原因。
executor修改promise的状态
前面我们提到,promise的状态是私有的,外部无法更改,只能内部处理。在我们吃创建一个promise实例的时候,会传入一个executor函数,而这个函数需要提供两个参数,如下

const p1= new Promise((resolved,rejected)=>{
    resolved()
})
console.log(p1)//Promise {<fulfilled>: undefined}
const p2 = new Promise((resolved,rejected)=>{
    rejected()
})
console.log(p2)//Promise {<rejected>: undefined}

可以看到,执行了resolved之后,状态机就会从pending->fulfilled,执行rejected之后从pending->rejected 除此之外,执行了rejected之后,promise还会抛出一个错误,// Uncaught (in promise) undefined executor是一个同步执行的回调,所以在上述代码中,在promise初始化的时候,状态已经被改变了,实际上的场景,修改状态是在一个异步操作中进行的

const p1= new Promise((resolved,rejected)=>{
    setTimeout(() => {
    resolved()
    }, 0);
   })
   console.log(p1)//Promise {<pending>}

当然,状态机只能修改一次,所以resolved和rejected如果多次执行,只会生效第一个执行的。 所以一个例子:为了避免异步操作时间过久卡死,可以设置一个超时自动结束的逻辑。

const p1= new Promise((resolved,rejected)=>{
setTimeout(() => {
  rejected()
}, 5000);

})
console.log(p1)//Promise {<pending>}
setTimeout(() => {
console.log(p1)//Promise {<rejected>: undefined}
}, 6000);

Promise.resolve()
promise的状态并非一开始就只能设置为pending,通过调用**Promise.resolve(),**可以得到一个resolved状态的Promise实例。下面的两个Promise是一样的

let p1 = new Promise((resolve, reject) => { resolve() })
let p2 = Promise.resolve()

而Promise.resolve()接收一个值,这个值就是作为resolved状态的值。实际上,Promise.resolve()可以把任意一个值转唯一个Promise

let p1 = Promise.resolve()
let p2 = Promise.resolve(2)
let p3 = Promise.resolve(2,3,4)
console.log(p1)//Promise {<fulfilled>: undefined}
console.log(p2)//Promise {<fulfilled>: 2}
console.log(p3)//Promise {<fulfilled>: 2},多个参数会忽略后面的。

即使Promise.resolve()的参数是一个错误对象,仍会将其转为一个resolved的promise实例

let p1 = Promise.resolve(new Error("this is err"))
console.log(p1)//Promise {<fulfilled>: Error: this is err

对于已经是resolved状态的promise实例,它具有幂等性,实际上只是做了一层控包装

  let p1 = Promise.resolve(1)
  let p2  = Promise.resolve(p1)
  console.log(p1===p2)//true

Promise.reject()
这个方法大体上和resolve方法是一致的,关键点在于,它不存在幂等性,如果你给它一个Promise,它仍会将其当作rejected的结果

let p1 = Promise.reject(1)
let p2  = Promise.reject(p1)
console.log(p2)//Promise {<rejected>: Promise}
console.log(p1===p2)//false

在Promise中,rejected的错误并没有抛出到同步代码的线程中所以trycatch无法捕获,所以一旦开启异步,唯一与其交互的方式就是使用异步结构,也就是说使用异步的方法。


try {
  throw new Error("Foo")
} catch (error) {
  console.log(1)
}
try {
  Promise.reject(new Error("Foo"))
} catch (error) {
  console.log(2)
}
//1
//Uncaught (in promise) Error: Foo

Promise的实例方法

thenable接口
promise的任何实例都有一个then方法,这个方法被认为实现了thenable接口。

class Thenable{
  then(){}
}

Promise.propertype.then()
Promise.prototype.then()是为Promise添加处理逻辑的主要方法。它接收两个参数,onResloved和onRejected的处理函数。这两个参数都是可选的,分别会在resolved和rejected的时候执行。

 const p1 = new Promise((resolved,rejected)=>{
    setTimeout(() => {
      resolved()
    }, 1000);
  })
  const p2 = new Promise((resolved,rejected)=>{
    setTimeout(() => {
      rejected()
    }, 1000);
  })
  p1.then((result) => {
    console.log("p1 is resolved")//p1 is resolved
  },()=>{
    console.log("p1 is rejected")
  })
  p2.then(()=>{
     console.log("p1 is resolved")
  },rejected=>{
    console.log("p2 is rejected")//p2 is rejected
  })

因为状态只会更改一次,所以两个方法一定只会执行一个。如果只想调用onRejected,那么第一个参数可以传undefined,避免创建不必要的对象。 而then会返回一个Promise实例,他的状态有四种情况

  1. then的实例中什么操作都没有,then中也没有任何操作,则是一个pending状态
  2. 如果then的实例执行了resolve或者reject,then中没有任何返回,就是对应的resolved或者rejected
  3. 如果then中onResolved或者onRejected有返回值,就相当于Promise.resolved(value)的状态,如果有报错,则会返回rejected
 const p1 = new Promise(() => {})
    const p2 = new Promise((resolve) => {resolve(1)})
    const p3 = new Promise((resolve,reject) => {resolve(1)})
    const p4 = new Promise((resolve,reject) => {reject(1)})
    const then1 = p1.then()
    const then2 = p2.then()
    const then3 = p3.then(()=>2)
    const then4 = p4.then()
  setTimeout(() => {
    console.log(then1)
    console.log(then2)
    console.log(then3)
    console.log(then4)
  }, 0);

Promise.propertype.catch()
这个方法其实是Promise.propertype.then(null,onRejected)的语法糖:

const p1 = Promise.reject()
function onRejected() {
  console.log("this is reject")
}
p1.catch(onRejected)//this is reject
p1.then(null, onRejected)//this is reject

Promise.propertype.finally()
Promise.propertype.finally()是promise的一个用于处理无论rejected或是resolved状态都能执行的处理程序,可以很好的避免在onResolved和onRejected中出现冗余代码

const p1 = Promise.resolve()
const p2 = Promise.reject()
p1.finally(()=>{
  console.log("this is finally")//this is finally
})
p2.finally(()=>{
  console.log("this is finally")//this is finally
})

它也会返回一个promise,但是它的promise与自身返回值无关,通常是父promise的状态,当finally内部出现错误,则会返回一个rejected的状态

const p1 = new Promise((resolve, reject) => { resolve("foo") })
const p2 = Promise.reject("bar")
const p3 = Promise.resolve("hello")
const f1 = p1.finally()
const f2 = p2.finally(() => { return "bar123"})
const f3 = p3.finally(() => { throw new Error("hello") })
setTimeout(() => {
  console.log(f1)//Promise {<fulfilled>: 'foo'}
  console.log(f2)//Promise {<rejected>: 'bar'}
  console.log(f3)//Promise {<rejected>: Error: hello
}, 0);

代码执行顺序
在then中的onResolved和onRejected会被放入一个异步队列中执行,所以它们总是会延迟于其他同步代码块

const p = Promise.resolve()
p.then(() => {
  console.log("this is then")
})
console.log("this is code")
//this is code
//this is then

虽然then在code之前,但是then的回调却晚于外面的同步代码块,这个是因为promse的onResolved和onRejected是一个微任务,关于微任务宏任务在这里不做过多解释。 如果给promise添加了多个处理程序,其结果会按照在微任务队列中的顺序执行

const p1 = Promise.resolve()
const p2 = Promise.reject()
p1.then(() => {
  setTimeout(() => {
    console.log("p1 then 1")
  }, 0);
})
p1.then(() => {
  setTimeout(() => {
    console.log("p1 then 2")
  }, 0);
})
p2.catch(() => {
  setTimeout(() => {
    console.log("p2 catch 1")
  }, 0);
})

p2.catch(() => {
  setTimeout(() => {
    console.log("p2 catch 2")
  }, 0);
})
p1.finally(()=>{
  setTimeout(() => {
    console.log("p1 finally 1")
  }, 0);
})
p1.finally(()=>{
  setTimeout(() => {
    console.log("p1 finally 2")
  }, 0);
})
//p1 then 1
//p1 then 2
//p2 catch 1
//p2 catch 2
//p1 finally 1
//p1 finally 2

传值
在resolve和reject的时候,传递的值会在onResolved和onRejected中被接收到

const p1 = new Promise((resolve,reject)=>{
  resolve("foo")
})
const p2 = new Promise((resolve,reject)=>{
  reject("bar")
})
p1.then(value=>{
  console.log(value)//foo
})
p2.catch(value=>{
  console.log(value)//bar
})

连锁的promise和合成的Promise

因为then、catch、finally会返回一个promise,因此我们可以链式的调用他们,并将值传递下去。

const p1 = new Promise((resolve, reject) => {
  resolve(1)
})
p1.then(value =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(value)
      resolve(++value)
    }, 1000);
  })
).then(value =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(value)
      resolve(++value)
    }, 1000);
  })
).then(value =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(value)
      resolve(++value)
    }, 1000);
  })
)
//1
//2
//3

这个是链式调用的例子,实际场景中还有组合调用的例子,这就涉及到了Promise.all()和Promise.race()方法。 Promise.all()
这个方法接受一个异步的数组,返回一个Promise实例,它的状态有以下几种情况:

  1. 如果数组中所有的异步都是resolved,则会将结果按照顺序存入一个数组,状态为resolved并返回数组
  2. 如果其中有一个异步的状态为rejected,则状态为rejected,并接受该rejected异步的值
  3. 如果传入的不是不是数组,则会报错,如果是数组,数组中包含有非Promise,则将该项实用Promise.reslove()包裹
 const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 1000);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3)
  }, 1500);
})
const a = Promise.all([1, p1, p2])
setTimeout(() => {
  console.log(a)//Promise {<fulfilled>:[1,2,3]
}, 2000);
//-------------------------------------------------------
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 1000);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(3)
  }, 1500);
})
const a = Promise.all([1, p1, p2])
setTimeout(() => {
  console.log(a)//Promise {<rejected>:3
}, 2000);

Promise.race()
这个方法的参数和all一样,不同的是,它会返回一个新的promise,包含了数组中最先为resolved或者rejected状态的值

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(2)
  }, 1000);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3)
  }, 1500);
})
const a = Promise.race([p1, p2])
setTimeout(() => {
  console.log(a)//Promise {<rejected>: 2}
}, 2000);
//————————————————————————————————————————————————————————————————
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 1000);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(3)
  }, 1500);
})
const a = Promise.race([p1, p2])
setTimeout(() => {
  console.log(a)//Promise {<fulfilled>: 2}
}, 2000);

它通常可以用来做一个超时操作

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 3000);
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("timeout")
  }, 2000);
})
const a = Promise.race([p1, p2])
setTimeout(() => {
  console.log(a)//Promise {<rejected>: 'timeout'}
}, 2000);

可取消的promise

我们在实际开发的时候,也会遇到异步已经发起,但是其结果已经不需要的情况,这个时候如果能取消这个异步就好了,这种逻辑在一些第三方库是存在的,比如Bluebird,但是es6的promise被认为是“激进的”:只要逻辑开启,就无法终止。 但是实际上我们可以在现有实现的基础上增加一层封装,以实现可以取消的功能:“取消令牌”。 下面是一个例子:

 class CancelToken {
      constructor(cancelFn) {
        this.promise = new Promise((resolve, reject) => {
          cancelFn(resolve)
        })
      }
  }

案例:两个按钮,start和end,点击start的时候模拟了一个异步请求,1s之后会正常调用结束,end按钮绑定了一个可以取消该异步的事件,在start点击1s之内,点击end,可以模拟取消这个异步。

<button id='start'>start</button>
  <button id='cancel'>cancel</button>
  <script>
    class CancelToken {
      constructor(cancelFn) {
        this.promise = new Promise((resolve, reject) => {
          cancelFn(()=>{
            resolve()
            console.log("取消")
          })
        })
      }
    }
    const startBtn = document.getElementById("start")
    const cancelBtn = document.getElementById("cancel")
    function cancle(delay) {
      return new Promise((resolve, reject) => {
        const id = setTimeout(() => {
          console.log("正常结束")
          resolve()
        }, delay);
        const cancelToken =new CancelToken((cancelCallback)=>
          cancelBtn.addEventListener("click",cancelCallback)
        )
        cancelToken.promise.then(()=>clearTimeout(id))
      })
    }
    startBtn.addEventListener("click",()=>cancle(1000))
  </script>

异步函数async/await

异步函数,也就是“async/await”语法关键字,是es8新增的一个语法,其特性就是可以让异步代码像同步代码一样书写。 这是Promise的常规写法,异步的返回值会在then方法的onResolved的第一个参数中拿到,这样所有需要依赖这个异步获取的数据的逻辑,都要写在then方法中:

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("bar")
  }, 1000);
})
p.then(value => {
  console.log(`value is ${value}`)
})

这个是实用async/await的写法:通过在async中使用await获取异步的返回结果,可以赋值给一个变量,后续需要该结果的代码都可以像同步代码一样正常书写。

async function getValue() {
  const value = await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("bar")
    }, 1000);
  })
  console.log(`value is ${value}`);
}
getValue()

async
是一个关键字,可以在函数声明、函数表达式、箭头函数上声明,如果函数有返回值,则会使用Promise.resolve包裹,如果没有默认包裹一个undefined。当然,then中的回调仍然会先放入异步任务列表中在同步代码块执行之后再执行。

async function getValue() {
  console.log(1)
  return 2
}
getValue().then((value) => {
  console.log(value)
})
console.log(3)
//1
//3
//2

当函数中发生错误,则会进入catch中

async function getValue() {
  throw new Error("foo")
  return 2
}
getValue().catch((value) => {
  console.log(value)
})

await
异步的特点就是不会立马完成任务,所以需要能暂停并回复执行的能力。而await则提供了这种能力,它会先暂停异步函数代码的执行,直到异步函数的状态落定(pending发生改变) 前面的例子:

async function getValue() {
  const value = await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("bar")
    }, 1000);
  })
  console.log(`value is ${value}`);
}
getValue()

await会暂停异步函数后面的代码,让出javascript运行时的执行线程,这个行为和生成器的yeild是一致的。await会尝试“解包”,并将解包之后的值传给表达式 await 通常需要在等待一个Promise的对象,而如果是常规值,则会当做已经落定的Promise对象

async function getValue() {
  console.log(1)
  const a =  await 2
  console.log(a)
  console.log(3)
}
getValue()
//1
//2
//3

如果抛出了错误,得到一个rejected的Promise

async function getValue() {
  return await (()=>{ throw 3})()
}
console.log(getValue())
getValue()//Promise {<rejected>: 3}

await的限制
await只能在异步函数中使用,也就是在使用async关键字修饰的函数中使用,不能再顶级上下文如

async function log(params) {
  console.log(await Promise.resolve(3))
}
log();//3
//一样的效果
(async function () {
  console.log(await Promise.resolve(3))
})();//3

执行顺序

async function foo() {
  console.log(await Promise.resolve("foo"))
}
async function bar() {
  console.log(await "bar")
}
async function baz() {
  console.log('baz')
}
foo()
bar()
baz()
//baz
//bar
//foo

打印顺序和函数的执行顺序完全相反 await并非是等待一个值这么简单,实际上,当执行到await的时候,会中断函数的执行,当右边的值可用之后,会向消息队列中推送一个消息,此时再恢复到函数中所在的位置继续执行。 因此,即时await后面跟了一个可以立即使用的值,函数后面的部分也不会立即执行。

async function bar() {
  console.log("bar")
  await 1
  console.log("foo")
}
bar()
console.log("baz")

执行过程:

  1. 执行函数bar
  2. 打印出“bar”(在bar函数中)
  3. 遇到await,暂停执行,为 1 向消息队列中添加一个任务
  4. 打印“baz”
  5. 主线程结束,
  6. 从消息队列取出任务,恢复执行
  7. 打印出“foo”

平行原则
当我们需要使用多个await的时候,情况如下

function random(id) {
  const timeout = Math.random() * 1000
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${id} done`)
      resolve()
    }, timeout);
  })
}
async function foo() {
  const now = Date.now()
  for (let i=0;i<5 ;i++) {
    await random(i)
  }
  console.log(`finshed use ${Date.now() - now}`)
}
foo()
//0 done
//1 done
//2 done
//3 done
//4 done
//finshed use 5119

我们可以看到,五个异步会按照顺序执行,只有上一个执行完下一个才会开始执行。如果我们并不关心执行顺序,只想要各自的结果,那么这种方式就会损耗性能,增加不必要的等待时间 另一种更好的处理方式是,我们先一次性初始化完异步,让各自的异步先进行,之后对初始化过的异步用await取值,这样仍能保证取值的顺序,但是异步的过程却可以并行执行。

function random(id) {
  const timeout = Math.random() * 1000
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${id} done`)
      resolve()
    }, timeout);
  })
}
async function foo() {
  const now = Date.now()
  const promiseList = Array(5).fill(null).map((item,i) =>
    random(i)
                                             )
  for (const p of promiseList) {
    await p
  }
  console.log(`finshed use ${Date.now() - now}`)
}
foo()
//4 done
//2 done
//1 done
//3 done
//0 done
//finshed use 641

小结

JavaScript对于异步从提出到完善经历了很长时间,也有很多提案,到现在,我们可以编写出很漂亮的代码来完成异步,其中的原理和思想也值得我们去学习。 Promise为异步代码提供了清晰的抽象,已经广泛的应用到我们的代码中和很多框架的底层。作为一种可塑性很强的结构,Promise可以被序列化、连锁使用、复合、扩展和重组。

参考:《JavaScript高级程序设计(第四版)》