异步编程

什么是异步编程

与其他函数并行执行的函数称为异步。可以用下面的图简单理解。

javascript 等待异步请求完成 js等待异步方法结束_javascript

异步执行机制

当遇到需要等待的异步函数时,浏览器会将异步函数放入异步队列中,在异步操作完成后将其放回主线程。

javascript 等待异步请求完成 js等待异步方法结束_javascript 等待异步请求完成_02

回调地狱

以往的异步实现是采用回调函数的方法。

当需要嵌套回调时,会导致代码十分复杂,简称回调地狱。

Promise

promise基础

promise是es6中新增的对象,通过new来实例化。

在new时需要传入一个函数作为参数,这个函数被称为执行器

let t = new Promise(() => {});
console.log(t);

javascript 等待异步请求完成 js等待异步方法结束_vue.js_03

会得到这样一个对象。

状态机

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的内部值,多余的参数会被忽略。

javascript 等待异步请求完成 js等待异步方法结束_javascript_04

reject

与resolve相同,promise的初始状态也可以是拒绝。

let p = Promsie.reject("就是不想接受");

javascript 等待异步请求完成 js等待异步方法结束_前端_05

与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 的状态发生变化时,相应的处理程序会被推入异步队列,在当前线程的程序全部执行完毕后,开始执行异步队列中的程序。

javascript 等待异步请求完成 js等待异步方法结束_javascript 等待异步请求完成_06

值传递

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提供了两个静态方法用于期约合成。

  1. all方法
  • all方法接受一个数组。
  • 如果所有的 期约都被解决,则合成的期约的解决值就是所有期约解决值组成的数组。
  • 如果有期约拒绝,则合成的期约的值为第一个拒绝的理由,其他拒绝的期约也会被隐式处理。
  1. race方法
    race方法同样接受一个数组,他是一组期约中最先解决的期约的镜像。他会包装第一个落定的期约。

异步函数

async修饰符

为函数添加async修饰符可以将其变为异步函数,但是其内部的代码依然是同步执行的,async只是一个修饰符,本身对函数没有任何影响。

异步函数的返回值会被promise.resolve包装

await关键字

await关键字可以用在异步函数中,用于暂停异步函数的执行,然后将后面的语句包装进promise推入异步队列。

javascript 等待异步请求完成 js等待异步方法结束_前端_07


注意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