# JavaScript异步编程终极解决方案



## JavaScript异步终极解决方案

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行，而每条指令执行后也能立即获得存储在系统本地（如寄存器或系统内存）的信息。

相对地，异步行为类似于系统中断，即当前进程外部的实体可以触发代码执行。异步操作经常是必要的，因为强制进程等待一个长时间的操作通常是不可行的（同步操作则必须要等）。如果代码要访问一些高延迟的资源，比如向远程服务器发送请求并等待响应，那么就会出现长时间的等待。

### 早期的异步编程

在早期JavaScript中，对异步的处理方式并不理想，主要是采用回调函数的方式。

```javascript
request('https://example.com',function(err){
    if(err){ return ;}
})
```

#### 回调函数

回调函数字面意思就是"回过头调用的函数"，回调函数一般事先定义好，然后在某些时候"回过头"来调用，例如我们使用的`addEventListener`，第二个参数就是一个回调函数，特定事件触发后就会调用这个函数。

```javascript
let p = document.querSelector('#p');
p.addEventListener('click',funtion (){
   // 这是一个回调函数，当事件被触发时执行
})
```

回调函数的概念可以这样理解，执行一个异步操作（这个异步操作可能是一个点击事件，也可能是一个http请求），并且要在异步操作执行完毕后执行一些处理程序，但是由于我们不知道异步操作什么时候执行完，所以我们会先定义好一个函数但是不执行，等异步操作执行完后再调用。

```javascript
// 先把回调函数定义好
function fn(){
    // 执行某些操作
}
// 异步操作
function asyncFn(fn){
    // 执行某些代码
    // 执行完后执行回调函数
    fn()
}
asyncFn(fn);
```

回调函数在Node.js里用的非常多，Node.js大多数异步API都是基于回调函数的。

#### 回调地狱

用回调函数处理异步最大的问题就是容易造成回调地狱。

回调地狱是指多个回调函数深度嵌套，造成代码难以阅读和维护。

想象一下，如果我们需要从后端获取a接口、b接口、c接口中的数据，然后进行某些处理，那么我们的代码可能会像这样：

```javascript
request('https://example.com/a',function(err){
    if(err){ return ;}
    request('https://example.com/b',function(err){
        if(err){ return ;}
        request('https://example.com/c',function(err){
            if(err){ return ;}
            // 进行处理
        })
    })
})
```

显而易见，代码复杂且不易维护，开发体验非常不好。

### 解决回调地狱

JavaScript实现异步最大的问题就是容易出现回调地狱。

```javascript
const fs = require("fs");
fs.stat('./1.txt',(err,data)=>{
    if(err){
        // 错误处理
    }else{
        fs.readFile('./1.txt',(err,data)=>{
            if(err){
                // ...
            }else {
                // ...
            }
        })
    }
})
```

随着代码越来越复杂，函数嵌套的就越来越多，这是对于代码维护来说是一件非常痛苦的事情，所以我们会想能不能将里面的代码提取到外面来，比如这样

```javascript
const fs = require("fs");
fs.stat('./1.txt',(err,data)=>{
    if(err){
        // 错误处理
    }else{
       
    }
})
fs.readFile('./1.txt',(err,data)=>{
    if(err){
        // ...
    }else {
        // ...
    }
})
```

可惜这种行为很显然会有问题，因为`fs.stat`是一个异步操作，因此`fs.readFile`不会等到`fs.stat`执行完毕后才运行，它很可能在`fs.stat`还没执行完毕前就执行了，这就导致了逻辑错误。

这里要解决的问题就是当`fs.stat`执行完毕得出结果时，再执行`fs.reaFile`，这里很自然的可以想到用一个`flag`变量来标识`fs.stat`的执行状态，未执行完为`false`，执行完为`true`

```javascript
const fs = require("fs");
let flag = false;
fs.stat("./1.txt", (err, data) => {
	// fs.stat此时已执行完毕
	flag = true;
});
const timer = setInterval(() => {
	if (flag === true) {
		fs.readFile("./1.txt", (err, data) => {
			console.log("成功")
		});
		clearInterval(timer);
	}
}, 1);
// 成功
```

这种情况固然能解决我们的问题，但是`setInterval`这种方法太浪费资源了，不可能大规模使用。我们可以换个思路，可以用发布订阅模式来替代`setInterval`。

```javascript
const fs = require("fs");
// 实现发布订阅模式
function bus(fn) {
	if (!Array.isArray(fn)) {
		bus.fns = [fn];
	} else {
		bus.fns.push(fn);
	}
}
bus.run = function () {
	for (let fn of bus.fns) {
		fn();
	}
};

bus(function () {
	console.log("后执行");
});

fs.stat("./1.txt", (err, data) => {
	console.log("先执行");
	bus.run();
});
// 先执行
// 后执行
```

这样能达到我们的要求，我们可以改一下代码，让它更方便使用。

```javascript
const fs = require("fs");

function Bus(fn) {
	const fns = [];
	function resolve() {
		for (fn of fns) {
			fn();
		}
	}

	fn(resolve);

	this.then = function (resolveFn) {
		fns.push(resolveFn);
	};
}

const bus = new Bus(function (resolve) {
	fs.stat("./1.txt", (err, data) => {
		console.log("先执行");
		resolve();
	});
});

bus.then(function () {
	console.log("后执行");
});
// 先执行
// 后执行
```

改完之后使用起来方便了很多，我们来分析一下。

对于后执行的函数（即打印`后执行`的那个函数），我们将他们存放到一个数组`fns`里，方便遍历执行，而为了能够添加后执行函数，我们添加一个实例方法`then`，它会将参数（即要添加的后执行的函数）添加到数组`fns`中。

为了判断先执行的那个函数（即包裹`fs.stat`的那个函数）什么时候执行完成，我们创建了一个函数`resolve`，它会遍历执行`fns`中所有的函数，即所有的后执行的函数，当先执行的函数执行完后，我们调用`resolve`函数就行了，但是如何将resolve函数传递到先执行的函数中呢？很简单，我们将先执行的函数作为构造函数的参数传入，然后在执行这个函数时把`resolve`作为参数传入，而在先执行的函数里，我们可以灵活的放置`resolve`函数的位置。

事实上上面的方法只实现了一些简单的功能，还有不少地方没考虑，比如先执行的函数异常怎么办？上面的方法不能实现函数串联执行，即函数a执行完后执行函数b，函数b执行完后执行函数c。而Promise能解决这些的问题。Promise与上面的方法原理类似，但是功能更强大，逻辑更严谨，Promise是实现JavaScript异步编程的重要工具。

### Promise

ECMAScript 6新增的引用类型Promise来处理异步，Promise是一个构造函数，可以通过new操作符来实例化。创建Promise实例时需要传入执行器函数作为参数。

Promise有三种状态：`pending`、`fulfilled(也称resolve)`、`reject`,三者分别表示三种不同的状态：待定(pending)、兑现(fulfilled)、拒绝(reject)。

待定（pending）是期约的最初始状态。在待定状态下，期约可以落定（settled）为代表成功的兑现（fulfilled）状态，或者代表失败的拒绝（rejected）状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝，期约的状态就不再改变。

Promise有以下两个特点：

1. 对象的状态不受外界影响。`Promise`对象代表一个异步操作，有三种状态：`pending`（进行中）、`fulfilled`（已成功）和`rejected`（已失败）。只有异步操作的结果，可以决定当前是哪一种状态，任何其他操作都无法改变这个状态。这也是`Promise`这个名字的由来，它的英语意思就是“承诺”，表示其他手段无法改变。
2. 一旦状态改变，就不会再变，任何时候都可以得到这个结果。`Promise`对象的状态改变，只有两种可能：从`pending`变为`fulfilled`和从`pending`变为`rejected`。只要这两种情况发生，状态就凝固了，不会再变了，会一直保持这个结果，这时就称为 resolved（已定型）。

这两个特点很好理解，Promise三个状态分别表示异步操作是正在进行中、已经完成、已经失败，如果是正在进行中那么应该让它继续执行，如果是已完成则调用执行成功的处理程序`resolve`，如果是已失败则执行失败的处理程序`reject`。而这种状态的改变是不可逆的，一个异步操作执行成功就是成功，不可能一会成功一会又失败，这样很容易导致混乱。

```javascript
let pr = new Promise((resolve,reject)=>{
    // 创建promise时需要传入一个函数，函数第一个参数是resolve，第二个参数的reject
    
    // 模拟异步
    setTimeout(()=>{
        if(操作成功){
            // 将状态从pending改成fulfilled
            resolve('成功了')
        }else {
            // 将状态从pending改为reject
            reject('失败了')
        }
    },1000)
})

pr.then((res)=>{
    // fulfilled
    console.log(res) // 成功了
}).catch(err=>{
    // reject
    console.log(err) // 失败了
})
```

Promise接收一个执行器函数，这个函数有两个参数`resolve`和`reject`，这两个参数都是函数，可以被调用和传递参数，一旦调用`resolve()`就表示Promise对象由`pending`转变为`resolve`，会触发`Promise.then`第一个函数，而一旦调用`reject()`就表示Promise对象由`pending`转变为`reject`,会触发`Promise.then`第二个函数或`Promise.catch`函数（两者等价）。而转变后是不可再变的。

Promise也使用到了回调函数，例如`Promise.then`就接收回调函数作为参数，但是Promise却会不产生回调地狱，因为Promise是链式调用，这种方式让代码便于理解和维护，不会出现回调地狱。

```javascript
// 回调函数产生的回调地狱
request('https://example.com/a',function(err){
    if(err){ return ;}
    request('https://example.com/b',function(err){
        if(err){ return ;}
        request('https://example.com/c',function(err){
            if(err){ return ;}
            // 进行处理
        })
    })
})
// Promise的链式调用，这里假设pro是一个Promise对象
pro.then((res)=>{
    // 某些操作
    return 1;
}).then((res=>{
    // 某些操作
    // 上个Promise对象的返回值是此函数的参数
    console.log(res) // 1
})).then(res=>{
    // 某些操作
}).catch((err)=>{
    // Promise错误的处理程序
}).finally(()=>{
    // 无论Promise转变为resolve还是reject，都会触发此函数
    // finally函数无法判断Promise是resolve状态还是reject状态
})
```

Promise的链式调用不仅能够使代码更易维护，还有其他的优点。比如说在回调函数里需要对每个 回调函数判断是否出现错误，而在Promise中如果出现错误，会顺着调用链一直传递，直到遇到`catch()`才会被捕获。另外需要注意`try...catch`无法捕捉Promise中的错误。

Promise能够实现链式调用的秘诀在于，`Promise.then()`和`Promise.catch()`的返回值仍然是一个Promise对象。

```javascript
let pro = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve();
	}, 100);
});
let result = pro.then(() => {});
console.log(result);  // Promise { <pending> }
```

Promise更多语法这里不再赘述，详情可以看阮一峰老师的文章： [点我](https://wangdoc.com/es6/promise.html)

除此之外，Promise还提供多种静态方法。例如之前将的回调地狱，在Promise可以用更优雅的方式解决。

```javascript
// 这里为了方便，默认getA是封装了异步请求的promise的实例
Promise.all([getA,getB,getC]).then(res=>{
    // 所有请求都成功时才执行此函数
}).catch(err=>{
    // 有请求失败时
}).finnaly(()=>{
    // fulfilled和reject都会触发此函数
    // finally函数无法判断Promise是resolve状态还是reject状态
})
```

除了`all`方法外，Promise还有另外五种方法: `race`、`resolve`、`reject`、`allSettled`、`any`。

### 异步函数

ES2017 标准引入了 `async`函数，使得异步操作变得更加方便。

实现一个异步函数非常简单，只需要在函数声明前加上关键字`async`就可以了

```javascript
async function asyncFn(){    
    // 
}
```

单是这样的话，异步函数与普通函数没什么不同，异步函数里最重要的还是`await`关键字，它是实现异步函数的关键，而`await`关键字必须是在`async`函数中。

`await`关键字后面跟着的只能是Promise对象或者是原始值，当异步函数执行的时候，一旦遇到`await`就会先跳出函数，执行后面的语句，等到异步操作完成，再接着执行函数体内后面的语句。

```javascript
let pro = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve();
	}, 100);
});

async function asyncFn() {
	await pro;
	console.log(1);
}

asyncFn();
console.log(2);
// 2 1
```

当执行到`await pro;`语句时，会跳出异步函数，执行后面的语言打印2，等异步操作执行完成后在执行`await`后面的语句，打印1.

`await`关键字会返回Promise对象`resolve`的第一个参数。

```javascript
let pro = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve("promise");
	}, 100);
});

async function asyncFn() {
	let a = await pro;
	console.log(a);
}

asyncFn();
// promise
```

`async`函数中使用`try...catch`捕获异常，这点与Promise不同。需要注意的是，如果`await`后面的Promise对象进入的是`reject`状态，则异步函数执行会被中断。解决的方法是将`await`语句没有被包裹在`try..catch`中或者在Promise对象后添加`catch`捕获异常。

```javascript
let pro = new Promise((resolve, reject) => {
	setTimeout(() => {
		reject();
	}, 100);
});

async function asyncFn() {
	await pro;
}

asyncFn();
// 报错
// 正确的做法应该是将await语句包裹在try中
let pro = new Promise((resolve, reject) => {
	setTimeout(() => {
		reject("promise");
	}, 100);
});

async function asyncFn() {
    try {
         await pro;
    } catch (e) {
    }
}

asyncFn();
// 或者是
let pro = new Promise((resolve, reject) => {
	setTimeout(() => {
		reject("promise");
	}, 100);
});

async function asyncFn() {
	await pro.catch(() => {});
}

asyncFn();
```

`async`函数返回一个 Promise 对象，可以使用`then`方法添加回调函数。

```javascript
console.log(asyncFn());
// Promise { <pending> }
```

使用async函数需要注意的是，如果使用await，那么await语句所在的函数必须是async函数。也就是说嵌套函数中，最外层是async函数，内层函数是普通函数，那么就不能在内层函数中使用await关键字。

```javascript
// 错误写法
async function asyncFn(){
    await 1; // 正确
    setTimeout(()=>{
        await 1; // 错误，这个函数不是async函数
    },10)
}
// 正确写法，在setTimeout的回调函数前加上async
async function asyncFn(){
    await 1; // 正确
    setTimeout(async ()=>{
        await 1; // 正确
    },10)
}
```

`async`函数本质上是 `Generator`函数的语法糖。async函数将`Generator`函数的星号和`yield`改成了语义更明确的`async`和`await`，另外async函数内置执行器，可以自动执行，还有就是async函数的返回值是Promise对象。

异步函数处理异步操作的方式比Promise更优雅，例如在之前的Promise的链式调用，在异步函数中可以这样写。

```javascript
let getA = new Promise((resolve, reject) => {
	setTimeout(() => {
		console.log("getA");
		resolve();
	}, 1000);
});
let getB = new Promise((resolve, reject) => {
	setTimeout(() => {
		console.log("getB");
		resolve();
	}, 500);
});
let getC = new Promise((resolve, reject) => {
	setTimeout(() => {
		console.log("getC");
		resolve();
	}, 100);
});

async function asyncFn() {
	await getA;
	await getB;
	await getC;
}

asyncFn();
// getA getB getC
```
