[toc]
### [前期回顾]事件循环
#### 基本概念
- JS运行的环境称之为宿主环境。
- 什么是执行栈:call stack,执行栈是一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。整个JS运行时只会存在一个执行栈。
- JS在执行过程中会先在执行栈中建立一个全局上下文(相当于JS执行期预编译的过程)。
- JS在执行代码的过程中,每执行一个函数会在栈顶入栈一个该函数的执行上下文。
- JS引擎永远执行的是执行栈的最顶部。
- 什么是异步函数:异步函数是指某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。
- 什么是线程:线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流。JS是单线程执行,执行代码的过程中只有一个执行栈。
- 在浏览器的宿主环境中共包含5个线程:
1. JS引擎:负责执行执行栈的最顶部代码
2. GUI线程:负责渲染页面
3. 事件监听线程:负责监听各种事件
4. 计时线程:负责计时
5. 网络线程:负责网络通信
> 这5个线程中能给JS执行代码的只有1个,且jS执行线程和GUI线程虽为独立执行线程,但两者间会相互等待。
#### 什么是事件循环
当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。
JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环(event loop)。
举例说明(以浏览器为宿主环境):
1. JS线程在执行代码时,遇到了需要满足某些条件才会触发的函数,例如onclick事件触发A函数。
2. 此时并没有发生点击事件,JS会将A函数发送到事件监听线程,然后继续执行栈顶的上下文。
3. 事件监听线程收到JS发来的A函数会按照约定的条件(如监听哪个元素的点击事件)进行监听点击事件。
4. 当点击事件被触发,事件监听线程检测到条件满足,但其自身并不会执行A函数。会将A函数放入到事件队列中(event queue)。
5. 当JS执行栈中已没有上下文执行时,会从事件队列中取出第一个函数加入到执行栈中执行(即JS执行栈执行A函数)
> 这个从JS执行栈将需要满中条件才能触发的函数传给事件监听线程->事件监听线程检测到条件满足将需要执行的函数发送到事件队列->JS执行队列中已无执行上下文后,再从事件队列中取出第一个函数进行执行的过程。称为事件循环。
*象这种需要满足某些条件才能被执行的函数称为异步函数,异步函数一定会被放到事件队列中。这是与同步函数最根本的区别*
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:
- 宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
- 微任务(队列):MutationObserver,Promise产生的回调进入微队列
微任务队列相对于宏任务队列有执行优先权。
> MutationObserver用于监听某个DOM对象的变化
当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。
### 事件和回调函数的缺陷
我们习惯于使用传统的回调或事件处理来解决异步问题
事件:某个对象的属性是一个函数,当发生某一件事时,运行该函数
例如:
```js
dom.onclick = function(){
//事件代码
}
```
回调:运行某个函数以实现某个功能的时候,传入一个函数作为参数,当发生某件事的时候,会运行该函数。
例如:
```js
dom.addEventListener("click", function(){
//函数代码
})
```
>本质上,事件和回调并没有本质的区别,只是把函数放置的位置不同而已。
一直以来,该模式都运作良好。
直到前端工程越来越复杂...
目前,该模式主要面临以下两个问题:
1. 回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断的嵌套
2. 异步之间的联系:某个异步操作要等待多个异步操作的结果,对这种联系的处理,会让代码的复杂度剧增
### 异步处理的通用模型
ES官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景。
值得注意的是,为了兼容旧系统,ES6 并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的 API,使用该API,会让异步处理更加的简洁优雅。
理解该 API,最重要的,是理解它的异步模型
1. ES6 将某一件可能发生异步操作的事情,分为两个阶段:**unsettled** 和 **settled**
![](https://img.kancloud.cn/ca/c3/cac3f24264184b18b043c1902d25c416_698x252.png)
- unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
- settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转
事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。
2. ES6将事情划分为三种状态: pending、resolved、rejected
- pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
- resolved:已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
- rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误
既然未决阶段有权力决定事情的走向,因此,未决阶段可以决定事情最终的状态!
我们将 把事情变为resolved状态的过程叫做:**resolve**,推向该状态时,可能会传递一些数据
我们将 把事情变为rejected状态的过程叫做:**reject**,推向该状态时,同样可能会传递一些数据,通常为错误信息
**始终记住,无论是阶段,还是状态,是不可逆的!**
![](https://img.kancloud.cn/6d/a8/6da866a4fa60869e96c016d515747c51_859x468.png)
3. 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
- resolved状态:这是一个正常的已决状态,后续处理表示为 thenable
- rejected状态:这是一个非正常的已决状态,后续处理表示为 catchable
后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行
![](https://img.kancloud.cn/9c/d1/9cd163f9ce6c295edecb48457f3a6748_1163x609.png)
4. 整件事称之为Promise
![](https://img.kancloud.cn/5a/09/5a095a599067f867c01ce7e05fca4bed_1191x626.png)
**理解上面的概念,对学习Promise至关重要!**
### Promise的用法
#### Promise的基本概念
Promise是一个构造函数,Promise并没有消除回调,其只是定义了一种特定的模式。让我们按照该模式的执行顺序编写代码。
#### 创建一个Promise
```js
//创建一个Promise的标准写法
const pro = new Promise((resolve, reject)=>{
// 此处编写未决阶段的一些处理代码(例如:ajax的发送请求部份)
// 通过调用resolve函数将Promise推向已决阶段的resolved状态
// 通过调用reject函数将Promise推向已决阶段的rejected状态
// resolve和reject均可以传递最多一个参数,表示推向状态的数据,若需要传多个参数,将其包装成对象
})
//例如:
const pro = new Promise((resolve, rejcet)=>{
console.log('我要去买一台电脑'); //从此处开始就是未决阶段的过程代码/处理函数,这部份代码会被立即执行
setTimeout(()=>{
if (Math.random() < 0.2){
resovle('买到了') //此处根据特定的条件,将事件推向已决阶段。说明当满足指定的条件后结果已经产生了
}
else{
resovle('钱不够') //此处根据特定的条件,将事件推向已决阶段。说明当满足指定的条件后结果已经产生了
}
},2000)
})
//上面的Promise中定义的代码,是Promise处在未决阶段所执行的代码,会立即执行,用来定义根据条件变化将Promise推向哪个状态。
//若为已决阶段需要执行的代码,使用下面的方式
pro.then(data=>{
//这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
//data为状态数据,名称可以自定义,其值为未决阶段resolve返回的数据
}, err=>{
//这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
//err为状态数据,名称可以自定义,其值为在未决阶段reject返回的数据或未决阶段执行期间捕获的错误信息
})
//then和err中的代码均为异步函数,加入到事件队列的微队列中,会优先于宏队列执行。
```
>如何理解Promise中resovle和reject返回的结果:
>当向目标发出一个请求,返回的结果是由目标给出的结果,无论结果如何(例如:返回有数据的结果/返回没有数据的结果),至少目标做出了回应。这样的结果应该由resolve返回。Promise从未决阶段将状态推向已决resovle阶段,由Promise对象的then方法来处理结果。
>当向目标发出一个请求,返回的结果不是目标给出的,而是执行过程中出现的某些错误导致请求中断,无法收到目标给出的结果。这样的结果Promise会自动捕获到并由reject返回,也可以将错误手动由reject返回。Promise从未决阶段将状态推向已决reject阶段,由Promise对象的catch方法来处理结果。
#### Promise的相关细节
1. 未决阶段的处理函数是同步的,会立即执行
2. thenable和catchable函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列
3. pro.then可以只添加thenable函数,pro.catch可以单独添加catchable函数
4. 在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向rejected,并会被catchable捕获
5. 一旦状态推向了已决阶段,无法再对状态做任何更改
6. **Promise并没有消除回调,只是让回调变得可控**
### Promise的串联
当后续的Promise需要用到之前的Promise的处理结果时,需要Promise的串联
Promise对象中,无论是then方法还是catch方法,它们都具有返回值,返回的是一个全新的Promise对象,它的状态满足下面的规则:
1. 如果当前的Promise是未决的,得到的新的Promise是挂起状态
2. 如果当前的Promise是已决的,会运行响应的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中;如果后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中。
**后续的Promise一定会等到前面的Promise有了后续处理结果后,才会变成已决状态**
如果前面的Promise的后续处理,返回的是一个Promise,则返回的新的Promise状态和后续处理返回的Promise状态保持一致。
### Promise的其它相关API
#### 原型成员 (实例成员)
- then:注册一个后续处理函数,当Promise为resolved状态时运行该函数
- catch:注册一个后续处理函数,当Promise为rejected状态时运行该函数
- finally:[ES2018]注册一个后续处理函数(无参),当Promise为已决时运行该函数
#### 构造函数成员 (静态成员)
- resolve(数据):该方法返回一个resolved状态的Promise,传递的数据作为状态数据
- 特殊情况:如果传递的数据是Promise,则直接返回传递的Promise对象
```js
const pro = new Promise((resolve, reject)=>{
resolve(1)
})
//在某些情况下,如果定义的一个Promise没有什么具体的执行过程,直接返回resolve,可以等同于下面的代码
const pro = new Promise.resolve(1)
```
- reject(数据):该方法返回一个rejected状态的Promise,传递的数据作为状态数据。用法与上面的resolve相同。
- all(iterable):这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。
>该方法必须全部为resolve时才会触发成功
- race(iterable):当iterable参数里的任意一个子promise被成功或失败后,父promise马上会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象
>该方法会返回第一个有结果的Promise对象,无论该结果是resovle还是reject。
### async和await简介
async 和 await 是 ES2016 新增两个关键字,它们借鉴了 ES2015 中生成器在实际开发中的应用,目的是简化 Promise api 的使用,并非是替代 Promise。
#### async用法
目的是简化在函数的返回值中对Promise的创建
async 用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置,被修饰函数的返回结果一定是 Promise 对象。
```js
async function test(){
console.log(1);
return 2;
}
//等效于
function test(){
return new Promise((resolve, reject)=>{
console.log(1);
resolve(2);
})
}
```
#### await用法
**await关键字必须出现在async函数中!!!!**
await用在某个表达式之前,如果表达式是一个Promise,则得到的是thenable中的状态数据。
```js
async function test1(){
console.log(1);
return 2;
}
async function test2(){
const result = await test1();
console.log(result);
}
test2();
```
等效于
```js
function test1(){
return new Promise((resolve, reject)=>{
console.log(1);
resolve(2);
})
}
function test2(){
return new Promise((resolve, reject)=>{
test1().then(data => {
const result = data;
console.log(result);
resolve();
})
})
}
test2();
```
如果await的表达式不是Promise,则会将其使用Promise.resolve包装后按照规则运行