# 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
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://1425816423.gitbook.io/my-knowledge-base/qian-duan-ji-shu/javascript/es6/javascript-yi-bu-bian-cheng-zhong-ji-jie-jue-fang-an.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
