异步编程
什么是异步编程
与其他函数并行执行的函数称为异步。可以用下面的图简单理解。
异步执行机制
当遇到需要等待的异步函数时,浏览器会将异步函数放入异步队列中,在异步操作完成后将其放回主线程。
回调地狱
以往的异步实现是采用回调函数的方法。
当需要嵌套回调时,会导致代码十分复杂,简称回调地狱。
Promise
promise基础
promise是es6中新增的对象,通过new来实例化。
在new时需要传入一个函数作为参数,这个函数被称为执行器。
let t = new Promise(() => {});
console.log(t);
会得到这样一个对象。
状态机
promise是一个有状态的对象。他拥有以下的三种状态。
- 待定(pending)
- 兑现(fulfilled),也叫做解决(resolved)
- 拒绝(reject)
待定是promise的初始状态,在待定状态下,promise可能变为兑现或拒绝,也可能已知处于待定状态。
promise的状态时是不可逆的,当她由待定变为兑现或者拒绝之后就不会再发生变化。
promise的状态是私有的,不能被直接访问。
解决值和拒绝理由
promise有两种用途,一种是提供状态,另一种是根据状态返回不用的值。
- 当promise处于待定状态时,有一个内部值为undefined。
- 当promise处于兑现状态时,promise的内部值会变为resolve函数的参数。
- 当promise处于拒绝状态时,pormise的内部值会变为reject函数的参数,也就是拒绝的理由。
状态控制
通过reject和resolve方法可以控制promsie的状态。
resolve和reject会作为执行器函数的参数,执行器函数内的语句是同步执行的。
当执行器函数内调用resolve时,promise的状态会变为兑现。
当执行器函数内调用reject时,promise 的状态会变为拒绝。
因为promise的状态不可逆,所以当两个状态控制函数执行其中一个后,其他的会立刻失效。
resolve
promsie的初始状态并不是必须为待定。
通过下面的语句可以创建一个初始状态为兑现的promsie对象。
let p1 = new Promise((resolve , reject) => {resolve()});
let p2 = Promise.resolve("这里是一坨数据");
这两段函数是完全等价的,resolve的参数会作为promise的内部值,多余的参数会被忽略。
reject
与resolve相同,promise的初始状态也可以是拒绝。
let p = Promsie.reject("就是不想接受");
与resolve不同的是,resolve传入promise作为参数会形成空包装,而reject传入promise依然会是拒绝的理由。
实例方法
Thenable接口
在所有的js异步对象中都有一个then方法,这个then方法被认为是实现了Thenable接口
class Thenable {
then(){}
}
promise就实现了thenable接口,在后面的async/await部分会用到这个接口。
then方法
then方法可以接受两个函数作为参数
第一个参数在promise状态变为满足时执行,第二个在promise状态变为拒绝是执行,非函数参数会被忽略。
new Promise(() => {}).then(onres() , onreq());
- 无论执行哪一个方法,都会返回一个包装过的Promise对象。
- 除非返回拒绝的promise对象,或者是抛出错误,否则都会返回满足状态的promsie对象。
- promise对象的值就是函数的返回值,如果没有返回值则包装undefined
catch方法
catch方法本质上是then方法的一个语法糖,可以用来处理拒绝状态的promise。
他只接受一个函数作为参数,其他行为与then方法相同。
new Promsie(() => {}).catch(() => {})
finally方法
- 只要promise的状态发生变化,finally方法就会被执行,这个函数主要用来处理then和catch中的重复的代码。
- 因为finally方法与状态无关,所以返回的promise对象是父对象的传递。
非重入方法
上面介绍的三个方法都是非重入方法。
当promise 的状态发生变化时,相应的处理程序会被推入异步队列,在当前线程的程序全部执行完毕后,开始执行异步队列中的程序。
值传递
promise在执行相应的处理程序时可以进行传值。
在执行器函数中传入的两个函数可以在调用时接受参数,这个参数会传递到对用的处理程序中作为内部值或者拒绝理由。
let p1 = new Promise((res,req) => {
res(111);
}).then((data) => {
console.log(data); // 111
})
临近执行顺序
如果为promise添加了多个处理程序,当promise状态变化时,相应的处理程序会依次执行,无论是then,catch或者是finally。
promise连锁和合成
连锁
多个promise进行组合可以形成强大的逻辑,连锁就是将多个promise一个接一个的连接起来,形成一条链。
这种编程可以完美的解决之前的回调地狱问题。
比如之前的回调地狱的写法
delay = function(str , callback = null){
setTimeout( () => {
console.log(str);
callback && callback();
})
}
dealy("p1" , () => {
delay("p2" , () => {
delay("p3" , () => {
delay("p4")
})
})
})
就可以改写为
delay = function(){
return new Promise((resolve , reject) => {
console.log(str);
setTimeout(resolve , 1000);
})
}
delay("p1")
.then( () => delay("p2") )
.then( () => delay("p3") )
.then( () => delay("p4") )
期约合成
将多个promise合成一个称为期约合成。Promise提供了两个静态方法用于期约合成。
- all方法
- all方法接受一个数组。
- 如果所有的 期约都被解决,则合成的期约的解决值就是所有期约解决值组成的数组。
- 如果有期约拒绝,则合成的期约的值为第一个拒绝的理由,其他拒绝的期约也会被隐式处理。
- race方法
race方法同样接受一个数组,他是一组期约中最先解决的期约的镜像。他会包装第一个落定的期约。
异步函数
async修饰符
为函数添加async修饰符可以将其变为异步函数,但是其内部的代码依然是同步执行的,async只是一个修饰符,本身对函数没有任何影响。
异步函数的返回值会被promise.resolve包装
await关键字
await关键字可以用在异步函数中,用于暂停异步函数的执行,然后将后面的语句包装进promise推入异步队列。
注意await关键字只能用在异步函数中。
异步函数的策略
sleep函数
利用await可以实现java中的sleep函数
async function sleep(delay) {
return new Promise((resolve) => {
setTimeout(resolve,delay);
})
}
async function foo() {
const t0 = Date.now();
await sleep(1500); //等待1500毫秒
console.log(Date.now() - t0);
}
平行执行
如果多条语句的顺序不是必须的,那么可以先一次性初始化出所有的期约,然后使用await全部推入异步队列,先解决的期约先执行。可以减少等待的时间。
async function randomDelay(id) {
const delay = Math.random() * 1000;
return new Promise((resolve) => {
setTimeout(() => {
setTimeout(comsole.log , 0 , `${id} finished`);
resolve();
}, delay)
})
}
async function foo(){
const t0 = Date.now();
const p0 = randomDelay(0);
const p1 = randomDelay(1);
const p2 = randomDelay(2);
const p3 = randomDelay(3);
const p4 = randomDelay(4);
await p0;
await p1;
await p2;
await p3;
await p4;
setTimeout(console.log , 0 , `${Date.now() - t0}ms elapsed`)
}
foo(); //5个promise 的执行顺序是随机的。
期约串联
使用await关键字,使得promise 的链式编程更加简洁。
async addTwo(x) {return x + 2;}
async addThree(x) {return x + 3;}
async addFive(x) {return x + 5;}
async function addTen(x) {
for(const fn of [addTwo , addThree , addFive]){
x = await fn(x);
}
return x;
}
addTen(9).then(console.log) // 19