自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

多圖剖析公式 Async=Promise+Generator+自動執(zhí)行器

開發(fā) 前端
Javascript 異步編程先后經歷了四個階段,分別是Callback 階段,Promise 階段,Generator 階段和 Async/Await 階段。

大家好,我是二哥。

??上篇??既是 Node.js 的核心,也是理解今天這篇的基礎。對 event-loop ,Node.js 官網有下面這樣一段描述。希望上一篇能幫你更好地理解這句話。

The event loop is what allows Node.js to perform non-blocking I/O operations  despite the fact that JavaScript is single-threaded  by offloading operations to the system kernel whenever possible.

這篇我們來剖析 async 的實現機制。文章有點長,還略為燒腦。如果沒有耐心一次看完,建議分批次看。

異步編程的好處多多,主線程負責策略,工作線程負責機制,完美匹配 Unix 的設計哲學:策略和機制分離。發(fā)號施令的是策略,苦逼干活的是機制。

Javascript 異步編程先后經歷了四個階段,分別是 callback 階段,Promise 階段,Generator 階段和 Async/Await 階段。

callback 很快就被發(fā)現存在回調地獄和控制權問題,Promise 就是在這個時間出現的,用來解決這些地獄問題。Promise 用起來的感覺當然是比 callback 絲滑太多,但碼農們使用一段時間后發(fā)現它的使用體驗還是比不上同步代碼。

我們知道同步代碼有一個無論 callback 還是 Promise 都無法比擬的優(yōu)點:代碼是一行一行運行的。如果哪行代碼被阻塞了,CPU就暫停運行,直到阻塞解除后再繼續(xù)。也就說請求發(fā)生的地方和請求完成的位置是挨在一起的,雖然時間上有先后,但空間上卻是連續(xù)的。

那有沒有一種語法,能讓我們既享受到異步編程的好處,又能有同步編程那樣的體驗呢?當然有!它就是 async/await 。其實大家一直在用 async/await ,也早就感受到它的優(yōu)美了:兼具運行效率與結構扁平。

async function asynFn(){
// code block 1
let a1 = await ( Promise instance a ) // LINE-A
// code block 2 // LINE-B
return xxx
}
syncFn()

不過,對于上面這段簡單的代碼,有幾個問題不知道你想過沒?

  • LINE-A 處的 await 語句表示需要等待后面的表達式的結果。“等待”這兩個字意味著變量 a1 的求值和 LINE-B 處代碼的執(zhí)行時機被延后了。這個延后操作是怎么做到的呢?用 new Promise() 創(chuàng)建 Promise instance a 時我們需要給它設置一個函數。在這個函數里,當我們調用 resolved(data) 后,a 的狀態(tài)就會變?yōu)?fulfilled ,為什么變量 a1 的值就會變成我們調用 resolved(data) 時所設的實參 data 呢?
  • async 和圖 1 所示的單進程多線程模型之間是什么關系?
  • async 是怎么實現的?
async/await = Promise + Generator + 自動執(zhí)行器

這是二哥總結的公式。它揭示了 async/await  和 Promise / Generator 之間的關系。上車吧,帶著上面的幾個問題和這個公式。

1、event-loop

在開啟我們的旅程之前呢,還是要先來復習上一篇聊到的至關重要的概念:event-loop 。它是 Node.js 的核心。

Node.js 主線程和線程池的配合關系如下圖所示。主線程負責執(zhí)行 JS  code ,線程池里面的 work thread 負責執(zhí)行類似訪問 DB、訪問文件這樣的耗時費力的工作,它倆通過消息隊列協(xié)調工作。

這和餐館工作流程類似。餐館由一個長得漂亮的小姐姐招呼客人落座并負責收集來自各個餐桌的點單。每次收到一個點好的菜單,小姐姐會迅速地把它通過一個小窗口遞交給后廚。后廚那里有一個小看板,所有的點單都被陳列在看板上。廚師長根據單子的時間和內容安排不同的廚師燒菜。菜燒好后,再由小姐姐負責上菜。

圖片

圖 1:Node.js 主線程和工作線程關系圖

2、Promise

Promise 是什么?我想不需要二哥在這里做過多介紹了。下面是 Promise 的典型使用方法介紹:

const promise = new Promise(/*executor*/ function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
// 對變量 promise 的使用場景 1
promise.then(value => {
// success
})
.catch(error => {
console.log(error);
});
// 對變量 promise 的使用場景 2
promise.then();
// 對變量 promise 的使用場景 3
await promise;

對這段代碼,二哥想在這里說幾個重點:

  1. Promise 是一個 Class,所以需要用 new Promise() 來創(chuàng)建一個 Promise 對象。
  2. Promise 還是一個狀態(tài)機。它有三種狀態(tài)  pending,fulfilled(resolved) 和 rejected。狀態(tài)轉換只能是 pending 到 resolved 或者 pending 到 rejected,且狀態(tài)一旦轉換完成,不能再次轉換。
  3. 我們調用 Promise 的then() 方法時所提供的 onResolved / onRejected 函數均是 callback。只有當 Promise 的狀態(tài)改變后,它們才會被調用。再強調一遍:只有當狀態(tài)改變后,我們通過 then() 方法所設置的 callback 才會被調用。不過也有可能調用 then(onResolved, onRejected) 時,這倆 callback 之一會被立刻執(zhí)行:當執(zhí)行 then() 方法的時候,Promise的狀態(tài)已經轉換完成了。什么時候會發(fā)生這種情況呢?其實很簡單,創(chuàng)建 Promise 對象的時候,我們需要提供一個 callback ,如上面的代碼所示,這里我們稱這個 callback 為 executor。這個 executor 是會被立即執(zhí)行的,等它執(zhí)行完了,new Promise() 才會返回,這時我們才可以基于這個 Promise 對象進行鏈式調用。我們只要在 executor 里面調用 resolve / reject 就可以迫使 then() 立即執(zhí)行 onResolved, onRejected 了。

我們反過來過一遍下面的自問自答:

  • 問:onResolved / onRejected 函數什么時候會被執(zhí)行?答:當 Promise 的狀態(tài)改變的時候。
  • 問:Promise 的狀態(tài)什么時候改變?答:當我們在 executor 里面調用 resolved(value) 的時候。
  • 問:那我們什么時候需要調用 resolved(value) ?答:當我們的異步請求做完了的時候。
  • 問:異步請求由誰來負責完成?答:Worker thread 來負責完成異步請求,當 worker thread 的異步操作結束后,通過 event-queue 通知 Node.js 主線程,并在 event-loop 的下一個 tick 擇機執(zhí)行 callback 函數。

所以這個過程其實是發(fā)起異步請求和請求完成后的 callback 函數調用過程。這個過程完全遵循圖1 所示的流程。

3、Generator

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數完全不同。

圖片

圖 2:Generator 函數示例

let g = gen();
g.next(); // return { value: 300, done: false }
g.next(); // return { value: 400, done: false }
g.next(); // return { value: xxx, done: true }

圖 2 即為一個 Generator 函數。Generator 語法層面的東西不是這篇文章的重點。二哥把它與普通函數最明顯的區(qū)別寫在這里:

  • ?像 gen()? 這樣的函數調用,Generator 函數里的代碼不會被立即執(zhí)行,也即該函數 ① 位置的代碼不會被執(zhí)行。函數調用會立即返回一個迭代器。既然是迭代器,那么我們可以不斷地通過 g.next()? 來遍歷這個 Generator 內部的狀態(tài)。每次  g.next() 調用會返回 { value: xxx, done: xxx} 這樣的 object 。
  • Generator 函數還可以包含一個關鍵字 yield ,如 ② 和 ④ 處代碼所示。yield 會使得函數的執(zhí)行暫停。
  • ⑥ 處的語句雖然看起來是 return xxx? ,不過實際上該函數返回的卻是 { value: xxx, done: true } 這樣的結構。?

我們可以把 Generator 理解為一個狀態(tài)機。它的狀態(tài)會隨著 Generator 函數內部代碼的不斷執(zhí)行而改變。而我們可以通過 g.next() 來遍歷這些狀態(tài)。

(1)區(qū)分兩個重要的概念

有兩個重要的概念需要區(qū)分開來,這對理解 Generator 的精華非常重要:

  • yield 表達式
  • yield 語句

它們之間的關系如圖 3 所示。我還在圖中標出了 Generator 函數執(zhí)行暫停點,閱讀后文的時候如果被繞暈了,可以回到這里來看看。

圖片

圖 3:yield 表達式和 yield 語句對比

function * gen(){
let a = 1
let b = 2
let a1 = yield a+b // LINE-A
// ^ 第一次調用 next() 暫停的位置
a = a1 ?? 3
b = 4
let a2 = yield a*b // LINE-B
// ^ 再次調用 next() 暫停的位置
return a2 // LINE-C
}
// 以下為 Generator 函數調用者 caller
let g = gen()
let res = g.next() // 第一次調用 next() // LINE-D
// do something accroding to res.value // LINE-E
g.next() // 第二次調用 next()
g.next(); // return { value: xxx, done: true } // LINE-F

像這個代碼里面,LINE-A 處的 a+b 表達式稱為 yield 表達式,表達式的求值結果體現在每次  g.next() 所返回的 Object 的 value 屬性上,也即 {value : 3, done: false} 。而 yield a+b 稱為 yield 語句。那它的返回值是什么呢?默認情況下它返回 undefined,所以 LINE-A 這行代碼執(zhí)行完后, a1 的值為 undefined。注意我說的是:LINE-A 處的這個 yield 語句執(zhí)行完后,a1 的值才為 undefined 。

yield 表達式影響到的是 next() 方法調用的返回值,進而改變了調用者的行為,如 LINE-E 處的代碼執(zhí)行會被 res.value 影響。而 yield 語句影響到的是 LINE-A 處的變量 a1 ,進而改變了 Generator 函數本身的代碼行為,比如 a = a1 ?? 3 變量 a 的取值就會被影響到。實際上 LINE-A 的執(zhí)行被分成了兩個階段:

  • 第一次調用 next() 從函數起始處開始執(zhí)行,直到遇到 yield 停下來,我在代碼里標明了暫停點。將表達式 a+b? 求值后,第一次 next() 調用返回 {value : 3, done: false} 。
  • 第二次調用 next() 會從 LINE-A 暫停處繼續(xù)執(zhí)行,直到遇到 LINE-B 處的 yield 停下。將表達式 a*b? 求值后,第二次 next() 調用返回 {value : 12, done: false}。這中間 a1 被賦值一次。當然,它的值為 undefined。?

老讓 a1 為 undefined 多沒意思,我們可以通過在調用 next() 時傳進去一個參數來改變 yield a+b 這條 yield 語句的返回值,注意我說的是改變 yield 語句的返回值,不是 yield 表達式。就像 g.next(100) 這樣,這樣的話,在第二次調用過程中, a1 就變成 100 了。你猜,第二次調用 .next() 得到的 value 是多少?對,這次它是 400(100*4)。

不過這里有個限制,我們不能在第一次執(zhí)行  g.next() 的時候給它注入一個值。

示例中的 LINE-C 使得 Generator 函數執(zhí)行終止,故對它的遍歷也就終結了。接著剛才的例子,LINE-F 處最后一次 next() 調用得到的返回值是:{value : 400, done: true} 。

(2)執(zhí)行權禪讓

如果你還沒有暈的話,我們繼續(xù)。如果你暈了的話,返回上一步繼續(xù)讀。

你發(fā)現了,上面的代碼里,CPU 在執(zhí)行 Generator 函數的時候,暫停了兩次,且都是遇到 yield 這個關鍵詞的時候暫停的。

每次暫停的點都是在 yield 表達式求值結束之后,但 yield 語句返回之前。請結合二哥在示例中標注的位置,把這句話多讀幾次。

Generator 函數的執(zhí)行暫停意味著 next() 調用立即返回了,直到下一次 next() 調用,Generator 函數才又得到了可以繼續(xù)執(zhí)行的機會。

你有沒有發(fā)現一個有意思的事情?

  • 每次遇到 yield 暫停,意味著 Generator 函數把代碼的執(zhí)行權交出來了,通過 next() 返回這樣的契機,執(zhí)行權來到了 caller 手上。
  • caller 再次調用 next() 意味著 Generator 函數又得以恢復運行,也就是說 caller 又以 next() 調用這樣的契機把執(zhí)行權遞交回 Generator 函數。

二哥給這個過程取了一個好聽的名字:執(zhí)行權禪讓。

(3)用手動執(zhí)行器驅動 Generator

到目前為止,我們大概了解了 Generator 相比普通函數的鮮明特征:

  • 對它的調用會立即返回,返回給調用者的是一個迭代器。
  • Generator 函數自己不能自動運行,得通過 next() 啟動執(zhí)行,每次暫停后,還得再通過 next() 驅動它繼續(xù)往前走。
  • 每一次調用迭代器的 next() 會使得 Generator 獲得代碼執(zhí)行權,并被驅使繼續(xù)往前運行,直到遇到下一個 yield 關鍵詞或者 return 語句。
  • 遇到 yield 關鍵詞就意味著本次 next() 調用該返回了,也意味著 Generator 函數該交出代碼執(zhí)行權了。伴隨著 next() 返回的是 { value: xxx, done: xxx} 這樣的 object 。其中 value 部分是對 yield 表達式求值得到。
  • 我們還可以通過給 next() 傳遞參數從而控制 yield 語句的返回值。

我們把前文所提到的調用者寫得完整一些,如下圖手動執(zhí)行器旁邊的代碼塊所示,代碼的每一行我用紫色數字標記出來了。再把圖 1的示例代碼稍作修改,把 yield 表達式改為一個 Promise 對象,同樣地,代碼每一行我用黃色數字做了標記。后文我用紫 ① 表示左側代碼第一行,類似地用黃 ① 表示右側代碼第一行。

讓我們來看看用手動執(zhí)行器來驅動 Generator 的過程。整個過程從紫 ① 代碼 g = gen() 執(zhí)行開始,到紫 ④ 結束。我在圖 3 中詳細標注了每一次 g.next() 的調用所引發(fā)的代碼執(zhí)行權的更替以及 Generator 函數的暫停和恢復情況,還有 next() 調用的返回值。

在看這個時序圖的時候,希望你能注意到下面幾個細節(jié):

  • 紫 ② 至 紫 ④ 每一次對 next() 的調用都意味著手動執(zhí)行器把代碼執(zhí)行權交還給了 Generator 。而當 next() 調用返回后,意味著手動執(zhí)行器又獲得了代碼執(zhí)行權。
  • 紫 ② 處代碼 g.next()? 執(zhí)行所得到的 value 的數據類型是一個 Promise 對象 a 。所以調用者需要對其調用 then(onResolved) 并等待 onResolved 被執(zhí)行。
  • 紫 ③ 處代碼是在紫 ② 所設置的 onResolved callback 里執(zhí)行的。這意味著只有當 Promise 對象 a 的狀態(tài)轉換完成,Generator 才有機會拿到執(zhí)行權并繼續(xù)往前執(zhí)行。
  • 紫 ③ 處代碼 g.next(data)? 執(zhí)行的時候,傳入了一個 data 。這意味著當 Generator 重新獲得執(zhí)行權后,右側黃 ② 處的變量 a1 的值為 data 。我們來仔細想一下,data 是從哪里冒出來的呢?首先:右側黃 ② 處, yield 表達式 Promise 對象 a 先通過迭代器的遍歷回傳到了左側紫 ② 處;然后:在 Promise a 的 executor 里執(zhí)行 resolve(data)? 后,data 出現了;最后:再通過 next(data)? 的方式注入到 Generator ,改變了黃 ② 處 yield 語句的返回值,也就把 data 交到了變量 a1 的手上 。這真是一個非常巧妙的過程。通過這樣的過程,我們既利用前面所說的 Generator 的特征控制住了右側代碼的執(zhí)行節(jié)奏,還把左側代碼的執(zhí)行結果帶回給了右側。右側黃 ② 處的代碼,如果我們把 yield 改成 await ,剛才所說的過程是不是就實現了 await 的語義??
  • 紫 ④ 處代碼和紫 ③ 類似,我就不細說了。

圖片

圖 3:手動執(zhí)行器驅動 Generator 時序圖

4、自動執(zhí)行器

上面的手動執(zhí)行器用來解釋 Generator 的執(zhí)行過程可以,但沒有實用功能,因為 Generator 里面有多少個 yield 語句,就得手寫對應個數的 .value.then() ,想想就覺得很累。所以搞一個可以無視 Generator 里面 yield 語句個數的自動執(zhí)行器很有必要。

圖 5 右側就是這樣的自動執(zhí)行器。代碼源自阮一峰的《ECMAScript 6 入門》。執(zhí)行器的入口是右側紫 ⑦ 。很容易看懂,我就不多講了。

通過這樣的自動執(zhí)行器,我們可以驅動任意一個 Generator 函數,并在執(zhí)行權利的左、右側交換之間得到所需的數據。

圖片

圖 5:Genetaror + 自動執(zhí)行器

5、async / await

恭喜你,堅持到現在還沒有放棄。我們離終點不遠啦。

async 函數其實是 Generator 函數的語法糖。那它到底是如何給 Generator 包裹上了糖衣并投喂給我們的呢?且看圖 6 。

最右側的 async 函數和最左側的 Generator 在代碼結構上沒有任何區(qū)別,只是把關鍵詞 function * 替換成了 async function ,把 yield 替換成了 await 。通常情況下我們是 async/await 搭配使用的,await 只能用于 wait 一個 Promise 對象,所以 yield 表達式部分也是一個 Promise 對象。因為 Generator 沒法自己執(zhí)行的緣故,所以再搭配一個自動執(zhí)行器。

看到這里,你是不是猛然理解了:為什么 await 的目標必須是一個 Promise 對象(如果目標是原始類型的值如數值、字符串和布爾值等,會被自動轉成立即 resolved 的 Promise 對象)?

圖片

圖 6:async/await = Promise + Generator + 自動執(zhí)行器

6、代碼再回首

寫到這里,讓二哥來做一個總結:

async 函數本質上就是一個 Generator 函數,自動執(zhí)行器和 Generator 的合作過程其實就是不斷操作各種 Promise 對象的過程,而 Promise 對象又完整地基于圖 1 所示的 event-loop 在工作。

好了,我們再來看看??上一篇??開頭處的那段代碼。whileLoop_1() 和 whileLoop2() 這兩個函數都是 async 函數。將其抽絲剝繭后,我們會發(fā)現它們其實就是分別在 LINE-A 處和 LINE-B 處產生了異步請求。對于主線程而言,這樣的異步請求不會影響它繼續(xù)執(zhí)行其它的 JS code,所以我們能看到 CPU 不會陷入這兩個死循環(huán)中的任意一個。

'use strict';
async function sleep(intervalInMS)
{
return new Promise((resolve,reject)=>{
setTimeout(resolve,intervalInMS);
});
}
async function whileLoop_1(){
while(true){
try {
console.log('new round of whileLoop_1');
await sleep(1000); // LINE-A
continue;
} catch (error) {
// ...
}
console.log('end of whileLoop_1');
}
}
async function whileLoop_2(){
while(true){
try {
console.log('new round of whileLoop_2');
await sleep(1000); // LINE-B
continue;
} catch (error) {
// ...
}
console.log('end of whileLoop_2`');
}
}
whileLoop_1(); // LINE-C
whileLoop_2(); // LINE-D

我們看到無論是最早的 callback 還是 Promise, 再到 async/await 本質上都是異步編程模型,它們都是在充分利用 Node.js 的 event-loop 這個最核心的、最基礎的架構,最大化地提高并發(fā)度以提高系統(tǒng)資源利用率,同時在對程序員的編程友好度上也在不斷地提升。

Node.js 的 event-loop 這個架構是典型的事件驅動架構( event-driven architecture)。我們停下手中忙不完的工作,思考一下軟件運行的意義,梳理一下軟件開發(fā)模式的演進歷程,會發(fā)現無論是早期的單體巨石(monolithic)架構還是面向服務架構(service-oriented architecture),再到現在紅到發(fā)紫的微服務架構(microservice architecture),它們存在的意義以及進化的目的一直都沒有改變,那就是:盡一切可能,響應事件。

責任編輯:姜華 來源: 二哥聊云原生
相關推薦

2021-10-03 15:10:54

reduxsagaresolve

2022-04-29 08:41:40

開發(fā)應用程序執(zhí)行器

2024-12-04 10:47:26

2022-05-05 08:43:22

SQL執(zhí)行器參數

2023-10-08 10:21:11

JavaScriptAsync

2023-08-24 10:24:54

GitLabPodman

2009-04-15 19:12:31

2017-07-13 17:00:17

內置執(zhí)行器開發(fā)

2020-10-23 10:10:59

Promise前端代碼

2016-11-04 13:00:55

Asynces6Javascript

2021-07-21 10:48:03

物聯(lián)網傳感器執(zhí)行器

2020-10-16 08:26:07

JavaScript開發(fā)技術

2021-08-18 07:05:57

ES6Asyncawait

2017-04-10 15:57:10

AsyncAwaitPromise

2009-09-09 12:10:40

2024-06-28 11:39:21

2023-03-29 10:19:44

異步編程AsyncPromise

2024-09-02 14:12:56

2024-07-03 08:13:56

規(guī)則執(zhí)行器代碼

2024-12-17 00:00:00

Spring線程
點贊
收藏

51CTO技術棧公眾號