[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**

- unsettled: 未決階段,表示事情還在進行前期的處理,并沒有發生通向結果的那件事
- settled:已決階段,事情已經有了一個結果,不管這個結果是好是壞,整件事情無法逆轉
事情總是從 未決階段 逐步發展到 已決階段的。并且,未決階段擁有控制何時通向已決階段的能力。
2. ES6將事情劃分為三種狀態: pending、resolved、rejected
- pending: 掛起,處于未決階段,則表示這件事情還在掛起(最終的結果還沒出來)
- resolved:已處理,已決階段的一種狀態,表示整件事情已經出現結果,并是一個可以按照正常邏輯進行下去的結果
- rejected:已拒絕,已決階段的一種狀態,表示整件事情已經出現結果,并是一個無法按照正常邏輯進行下去的結果,通常用于表示有一個錯誤
既然未決階段有權力決定事情的走向,因此,未決階段可以決定事情最終的狀態!
我們將 把事情變為resolved狀態的過程叫做:**resolve**,推向該狀態時,可能會傳遞一些數據
我們將 把事情變為rejected狀態的過程叫做:**reject**,推向該狀態時,同樣可能會傳遞一些數據,通常為錯誤信息
**始終記住,無論是階段,還是狀態,是不可逆的!**

3. 當事情達到已決階段后,通常需要進行后續處理,不同的已決狀態,決定了不同的后續處理。
- resolved狀態:這是一個正常的已決狀態,后續處理表示為 thenable
- rejected狀態:這是一個非正常的已決狀態,后續處理表示為 catchable
后續處理可能有多個,因此會形成作業隊列,這些后續處理會按照順序,當狀態到達后依次執行

4. 整件事稱之為Promise

**理解上面的概念,對學習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包裝后按照規則運行