同步和異步
- ?同步(Synchronous)?
-
?定義:任務按順序依次執行,前一個任務完成前,后續任務必須等待。
-
?特點:阻塞性執行,程序邏輯直觀,但效率較低
- ?異步(Asynchronous)?
-
?定義:任務發起后無需等待結果,程序繼續執行其他操作,待任務完成后通過回調或事件通知處理結果。
-
?特點:非阻塞性執行,支持并發,資源利用率高
簡單來說,同步就是刷牙然后煮面,異步就是讓面在一邊煮著一邊跑去刷牙。
期約
期約是對尚不存在結果的一個替身。
在ES6中,期約是一種引用類型(Promise),使用new操作符實例化。
期約狀態機
期約對象有三種狀態,這些狀態是期約對象內置的,除了調用相應的API,否則不能對其進行更改。
-
pending(待定)
-
fulfilled(兌現)
-
rejected(拒絕)
新建一個期約對象,并且期約對象還沒進行任何操作時,期約對象的狀態為pending
,當期約對象已經被成功解決后,則轉為fulfilled
,而解決失敗則轉為rejected
,具體讓期約狀態轉換的函數后面介紹。
待定(pending)是期約的最初始狀態。在待定狀態下,期約可以落定(settled)為代表成功的兌現(fulfilled)狀態,或者代表失敗的拒絕(rejected)狀態。
無論落定為哪種狀態都是不可逆的。只要從待定轉換為兌現或拒絕,期約的狀態就不再改變。而且,也不能保證期約必然會脫離待定狀態。因此,組織合理的代碼無論期約解決(resolve)
還是拒絕(reject),甚至永遠處于待定(pending)狀態,都應該具有恰當的行為。
重要的是,期約的狀態是私有的,不能直接通過JavaScript檢測到。這主要是為了避免根據讀取到的期約狀態,以同步方式處理期約對象。
另外,期約的狀態也不能被外部JavaScript代碼修改。這與不能讀取該狀態的原因一樣:【期約故意將異步行為封裝起來,從而隔離外部的同步代碼】《JavaScript高級程序設計第四版》
如何理解期約對象
正如前面介紹的,期約對象是專門為異步編程而設計的,期約對象是對尚不存在結果的一個替身。
舉個例子,你參加了一場考試,試卷由他人進行批改(異步操作),而你在試卷批改的過程中是自由的,你可以吃飯睡覺,你也可以嘗試查詢試卷批改的狀態。
如果試卷還在批改當中,則返回pending
,如果已經批改結束,并且你已經通過了考試,返回fulfilled
,如果未通過考試,則返回rejected
。
理解了上述的場景,我們就能夠理解期約對象和期約對象的狀態機了。
-
期約對象代表了某一個異步操作。
-
期約狀態機代表了異步操作的完成狀態。
如何控制期約對象的狀態機
期約對象的狀態是私有的,只能通過期約對象內部的API進行操作。
向期約對象中傳入一個執行器函數(回調函數),期約對象會向執行器函數傳入兩個參數resolve
和reject
,調用resolve()
使期約狀態變為fulfilled
,調用reject()
使期約狀態變為rejected
const waiter1 = new Promise((resolve, reject) => { });const waiter2 = new Promise((resolve, reject) => resolve());const waiter3 = new Promise((resolve, reject) => reject());// 其中undefined表示期約對象在完成后期待一個返回值,但這里沒有給返回值console.log('waiter1:', waiter1); // waiter1: Promise {<pending>}console.log('waiter2:', waiter2); // waiter2: Promise {<fulfilled>:undefined}console.log('waiter3:', waiter3); // waiter1: Promise {<rejected>:undefined}
期約對象的靜態方法
-
Promise.resolve()
這個方法可以直接創建一個
fulfilled
狀態的期約對象,這個期約對象的值由傳入的參數指定。const settled1 = Promise.resolve(3)const settled2 = Promise.resolve('我是字符串')const settled3 = Promise.resolve(new Promise(() => {}))console.log(settled1) // Promise {<fulfilled>: 3}console.log(settled2) // Promise {<fulfilled>: '我是字符串'}console.log(settled3) // Promise {<pending>}
可以看到,我們傳入什么值,期約對象就會返回什么值。
但是如果我們傳入的是另一個期約對象,則會直接返回傳入的期約對象。
-
Promise.reject()
這個方法可以直接創建一個
rejected
狀態的期約對象,這個期約對象的值由傳入的參數指定。const settled1 = Promise.reject(3)const settled2 = Promise.reject('我是字符串')const settled3 = Promise.reject(new Promise(() => {}))console.log(settled1) // Promise {<rejected>: 3}console.log(settled2) // Promise {<rejected>: '我是字符串'}console.log(settled3) // Promise {<rejected>: Promise {<pending>}}
這個方法和
Promise.resolve()
類似,也會將傳入的值作為期約對象的值返回。但不同的是,如果傳入一個期約對象,那么這個期約對象也會作為期約對象的值返回。
調用reject()或者
Promise.reject()
都會拋出一個異步錯誤。同步代碼塊中的trycatch結構無法捕獲到異步錯誤,只有異步結構中才能捕獲異步錯誤期約的實例方法
-
實現Thenable方法
在ECMAScript暴露的異步結構中,任何對象都有一個then()方法。
-
Promise.prototype.then()
then()方法掛載在Promise的原型上,所以被所有Promise實例對象共享。
then()方法接收兩個回調函數參數,第一個參數在Promise對象的狀態落定為
fulfilled
時執行,第二個參數在Promise對象的狀態落定為rejected
時執行。如何理解then()方法
在平時寫js方法時,我們都是使用的同步代碼塊,也就是說,寫在后面的代碼一定后執行,比如我們在前面一行計算
let sum = 10 + 1
,那么我們就可以在這一行后面的任意位置輸出sum
,因為sum
的計算是寫在前面的,在同步代碼塊中,他已經被計算完畢了。現在我們使用了異步編程,我們已經知道Promise對象內置了一個狀態機,它用于通知自己是否執行完畢。
由于Promise是異步執行的,假如我們在同步代碼塊中讀取Promise對象,我們有可能獲取3種結果。如果我們需要打印Promise的返回值,我們不可能在同步代碼塊中不停的檢測Promise的狀態,這樣會導致后面的代碼無法執行,這就違背了異步編程的初衷。
所以我們使用then()方法,then()方法可以想象為一個觸發器,設置好then()方法后,只要Promise對象
settled
到了任意一種狀態,就會觸發then()方法中設定好的函數,這樣我們就可以異步的處理Promise的返回值而無需再在同步代碼塊中處理Promise的返回值了。const p1 = new Promise((resolve, reject) => {// 1秒后返回10 + 1的計算結果setTimeout(() => resolve(10 + 1), 1000);});p1.then(() => {console.log('我計算完成,被觸發了');console.log('我是then中的p1:',p1)}, () => { console.log('我計算失敗,被觸發了'); });console.log('我是同步代碼塊中的p1:',p1)
可以看到,同步代碼塊由于執行的比較快,已經運行到輸出Promise對象的值了,但是此時Promise對象還沒執行完,狀態為
pending
,而then中卻可以正常輸出Promise的返回值11,這是因為then()中的第一個參數只有在Promise對象狀態為fulfilled時才被調用。我們將then()方法的第一個參數稱為onResolved處理程序,第二個參數稱為onRejected處理程序。
-
.then()
返回的 Promise 狀態如何確定??回調返回值類型決定狀態
-
?返回普通值?(非 Promise 對象):新 Promise 會被
Promise.resolve()
包裝為 ?fulfilled 狀態p.then(() => 42); // 新 Promise 狀態:fulfilled,值:42
-
?拋出異常:新 Promise 變為 ?rejected 狀態,異常對象作為拒絕原因
p.then(() => { throw new Error("fail"); }); // 狀態:rejected,原因:Error對象
-
?返回 Promise 對象:新 Promise 將 ?繼承該 Promise 的狀態和值
p.then(() => Promise.reject("error")); // 新 Promise 狀態:rejected,原因:"error"
因此通過.then()方法返回的也是Promise對象,所以也有.then()方法,.then()方法可以進行鏈式調用
const p1 = new Promise((resolve, reject) => {// 3秒后返回10 + 1的計算結果setTimeout(() => resolve(double(1)), 1000);});p1.then(value => {return value}).then(value => {return double(value)}).then(value => {return double(value)}).then(value => console.log(value)) // 8
-
-
Promise.prototype.catch()
等于
then(null,() => {})
,也就是onRejected處理程序。 -
Promise.prototype.finally()
傳入finally()的回調函數能保證一定被執行,和try-catch-finally中的finally用法一致。
-
Promise.all()和Promise.race()
這兩個方法都可以傳入一個包含多個期約的可迭代對象,常見方法是傳入一個包含多個期約的數組。
-
Promise.all()
:會等待傳入的期約全部兌現后才兌現,如果有一個期約待定或者拒絕,則返回待定或者拒絕 -
Promise.race()
:會返回根據第一個兌現或者拒絕的期約決定狀態的新期約對象。
-
異步函數
通過剛剛期約對象的學習我們了解到,期約對象是異步執行的,因此如果想操作期約對象的流程必須要使用then()
方法。
但是這樣同樣導致了一個問題,同步代碼塊和異步代碼塊被完全的區分開了,從使用了期約對象開始,所有和這個期約對象有關的流程都要在then中實現,這會使得then()方法的函數體變得很大,并不好維護。
于是,從ES8開始引入了一組新的關鍵字async/await
用于解決這個問題。
基本概念
-
?
async
函數-
聲明方式:在函數前添加
async
關鍵字,如async function fetchData() {}
。 -
返回值:始終返回一個
Promise
對象。若函數返回非Promise
值(如字符串、數值),該值會被自動包裝為resolve
狀態的Promise
async function example() { return "Hello"; } example().then(console.log); // 輸出 "Hello"
-
-
?
await
關鍵字-
使用范圍:僅能在
async
函數內部使用。 -
功能:暫停當前
async
函數的執行,等待右側的Promise
完成(resolve
或reject
),并返回解析后的值async function fetchUser() {const response = await fetch('/api/user'); // 等待請求完成return response.json(); }
簡單來說,關鍵字
async
聲明了這個函數應該被異步執行,關鍵字await
表明被async
聲明的函數應該被停止執行,等到await
右側的表達式返回值后才被繼續執行。 -
async
被async
關鍵字標記的函數會返回一個Promise對象,如果返回的值不是Promise對象,則會使用Promise.resolve()對返回的值進行包裝。
async
函數的執行流程
- ?同步代碼的立即執行
-
?未遇到
await
時:async
函數內部的代碼會按照同步順序立即執行,與普通函數的行為完全一致。例如:async function demo() {console.log("A"); // 同步執行console.log("B"); // 同步執行 } demo(); console.log("C");
輸出順序為:
A → B → C
-
?本質:
async
函數被調用時,其函數體內的同步代碼會直接進入主線程的同步任務隊列,立即執行。
- ?**
await
對執行流程的干預**
-
?遇到
await
時:函數會暫停當前執行,將await
后的表達式(通常是Promise
)放入微任務隊列,并交出主線程控制權。此時,外部同步代碼會繼續執行。例如:async function demo() {console.log("A");await new Promise(resolve => setTimeout(resolve, 1000)); // 暫停console.log("B"); // 異步執行(微任務) } demo(); console.log("C");
輸出順序為:
A → C → (1秒后) B
-
?關鍵機制:
await
后的代碼會被封裝為微任務,等待當前同步代碼執行完畢后才會繼續執行。
async function heavyTask () {console.log("開始耗時操作");// 沒有awaitfor (let i = 0; i < 1e9; i++); console.log("耗時操作完成");}heavyTask();console.log("外部代碼");
async function heavyTask () {console.log("開始耗時操作");// 有awaitawait '123'for (let i = 0; i < 1e9; i++); console.log("耗時操作完成");}heavyTask();console.log("外部代碼");
可以看到,在await后的代碼才會作為異步代碼執行,否則async修飾的代碼會像普通函數一樣同步執行。
await
await關鍵字期待右側是一個實現了Thenable接口的對象。
但如果不是,則await不會等待,而是將右側視為一個已經fulfilled
的期約對象,直接返回。不會將值包裝為Promise對象
const func = async () => {console.log(await '123') }// 注意,123沒有被包裝為Promise對象func() // '123'
如果await右側是一個Promise對象,并且尚未settled,那么異步程序會在await處阻塞,停止運行直到右側的Promise對象已經fulfilled
或者rejected
。
異步函數的特質不會擴展到嵌套函數,await只能在async標記的函數中使用
如果需要進行并行優化,不要每調用一次async函數就等待await返回值,而是一次性將async函數全部調用后,再按照需要的順序等待await的返回值。
const asyncFunc1 = async () => { console.log(1); return 'a' }const asyncFunc2 = async () => { console.log(2); return 'b' }const asyncFunc3 = async () => { console.log(3); return 'c' }const asyncFunc4 = async () => { console.log(4); return 'd' }// 錯誤示范const run1 = async () => {console.log(await asyncFunc1()) console.log(await asyncFunc2()) console.log(await asyncFunc3()) console.log(await asyncFunc4()) }// 正確示范const run2 = async () => {const res1 = asyncFunc1()const res2 = asyncFunc2()const res3 = asyncFunc3()const res4 = asyncFunc4()console.log(await res1) console.log(await res2) console.log(await res3) console.log(await res4) }
在錯誤示范中,每次調用async函數都等到async函數返回后才執行下一個async函數。
而正確示范中,將所有async函數全部執行后,再等待async函數的返回值。
正確示范也可以通過Promise.all()來實現。