<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          「譯」更快的 async 函數(shù)和 promises

          共 9559字,需瀏覽 20分鐘

           ·

          2020-11-12 21:40



          來源:https://www.yuque.com/es2049/blog

          譯自:Faster async functions and promises

          JavaScript 的異步過程一直被認(rèn)為是不夠快的,更糟糕的是,在 NodeJS 等實(shí)時(shí)性要求高的場(chǎng)景下調(diào)試堪比噩夢(mèng)。不過,這一切正在改變,這篇文章會(huì)詳細(xì)解釋我們是如何優(yōu)化 V8 引擎(也會(huì)涉及一些其它引擎)里的 async 函數(shù)和 promises 的,以及伴隨著的開發(fā)體驗(yàn)的優(yōu)化。

          溫馨提示: 這里有個(gè) 視頻:https://www.youtube.com/watch?v=DFP5DKDQfOc,你可以結(jié)合著文章看。

          異步編程的新方案

          從 callbacks 到 promises,再到 async 函數(shù)

          在 promises 正式成為 JavaScript 標(biāo)準(zhǔn)的一部分之前,回調(diào)被大量用在異步編程中,下面是個(gè)例子:

          function?handler(done)?{
          ??validateParams((error)?=>?{
          ????if?(error)?return?done(error);
          ????dbQuery((error,?dbResults)?=>?{
          ??????if?(error)?return?done(error);
          ??????serviceCall(dbResults,?(error,?serviceResults)?=>?{
          ????????console.log(result);
          ????????done(error,?serviceResults);
          ??????});
          ????});
          ??});
          }

          類似以上深度嵌套的回調(diào)通常被稱為「回調(diào)黑洞」,因?yàn)樗尨a可讀性變差且不易維護(hù)。

          幸運(yùn)地是,現(xiàn)在 promises 成為了 JavaScript 語言的一部分,以下實(shí)現(xiàn)了跟上面同樣的功能:

          function?handler()?{
          ??return?validateParams()
          ????.then(dbQuery)
          ????.then(serviceCall)
          ????.then(result?=>?{
          ??????console.log(result);
          ??????return?result;
          ????});
          }

          最近,JavaScript 支持了 async 函數(shù),上面的異步代碼可以寫成像下面這樣的同步的代碼:

          async?function?handler()?{
          ??await?validateParams();
          ??const?dbResults?=?await?dbQuery();
          ??const?results?=?await?serviceCall(dbResults);
          ??console.log(results);
          ??return?results;
          }

          借助 async 函數(shù),代碼變得更簡(jiǎn)潔,代碼的邏輯和數(shù)據(jù)流都變得更可控,當(dāng)然其實(shí)底層實(shí)現(xiàn)還是異步。(注意,JavaScript 還是單線程執(zhí)行,async 函數(shù)并不會(huì)開新的線程。)

          從事件監(jiān)聽回調(diào)到 async 迭代器

          NodeJS 里 ReadableStreams 作為另一種形式的異步也特別常見,下面是個(gè)例子:

          const?http?=?require('http');

          http.createServer((req,?res)?=>?{
          ??let?body?=?'';
          ??req.setEncoding('utf8');
          ??req.on('data',?(chunk)?=>?{
          ????body?+=?chunk;
          ??});
          ??req.on('end',?()?=>?{
          ????res.write(body);
          ????res.end();
          ??});
          }).listen(1337);

          這段代碼有一點(diǎn)難理解:只能通過回調(diào)去拿 chunks 里的數(shù)據(jù)流,而且數(shù)據(jù)流的結(jié)束也必須在回調(diào)里處理。如果你沒能理解到函數(shù)是立即結(jié)束但實(shí)際處理必須在回調(diào)里進(jìn)行,可能就會(huì)引入 bug。

          同樣很幸運(yùn),ES2018 特性里引入的一個(gè)很酷的 async 迭代器 可以簡(jiǎn)化上面的代碼:

          const?http?=?require('http');

          http.createServer(async?(req,?res)?=>?{
          ??try?{
          ????let?body?=?'';
          ????req.setEncoding('utf8');
          ????for?await?(const?chunk?of?req)?{
          ??????body?+=?chunk;
          ????}
          ????res.write(body);
          ????res.end();
          ??}?catch?{
          ????res.statusCode?=?500;
          ????res.end();
          ??}
          }).listen(1337);

          你可以把所有數(shù)據(jù)處理邏輯都放到一個(gè) async 函數(shù)里使用 for await…of 去迭代 chunks,而不是分別在 'data''end' 回調(diào)里處理,而且我們還加了 try-catch 塊來避免 unhandledRejection 問題。

          以上這些特性你今天就可以在生成環(huán)境使用!async 函數(shù)從 Node.js 8 (V8 v6.2 / Chrome 62) 開始就已全面支持,async 迭代器從 Node.js 10 (V8 v6.8 / Chrome 68) 開始支持。

          async 性能優(yōu)化

          從 V8 v5.5 (Chrome 55 & Node.js 7) 到 V8 v6.8 (Chrome 68 & Node.js 10),我們致力于異步代碼的性能優(yōu)化,目前的效果還不錯(cuò),你可以放心地使用這些新特性。

          上面的是 doxbee 基準(zhǔn)測(cè)試,用于反應(yīng)重度使用 promise 的性能,圖中縱坐標(biāo)表示執(zhí)行時(shí)間,所以越小越好。

          另一方面,parallel 基準(zhǔn)測(cè)試 反應(yīng)的是重度使用 Promise.all() 的性能情況,結(jié)果如下:

          Promise.all 的性能提高了八倍!

          然后,上面的測(cè)試僅僅是小的 DEMO 級(jí)別的測(cè)試,V8 團(tuán)隊(duì)更關(guān)心的是 實(shí)際用戶代碼的優(yōu)化效果。

          上面是基于市場(chǎng)上流行的 HTTP 框架做的測(cè)試,這些框架大量使用了 promises 和 async 函數(shù),這個(gè)表展示的是每秒請(qǐng)求數(shù),所以跟之前的表不一樣,這個(gè)是數(shù)值越大越好。從表可以看出,從 Node.js 7 (V8 v5.5) 到 Node.js 10 (V8 v6.8) 性能提升了不少。

          性能提升取決于以下三個(gè)因素:

          • TurboFan,新的優(yōu)化編譯器 ?
          • Orinoco,新的垃圾回收器 ?
          • 一個(gè) Node.js 8 的 bug 導(dǎo)致 await 跳過了一些微 tick(microticks) ?

          當(dāng)我們?cè)?Node.js 8 里 啟用 TurboFan 的后,性能得到了巨大的提升。

          同時(shí)我們引入了一個(gè)新的垃圾回收器,叫作 Orinoco,它把垃圾回收從主線程中移走,因此對(duì)請(qǐng)求響應(yīng)速度提升有很大幫助。

          最后,Node.js 8 中引入了一個(gè) bug 在某些時(shí)候會(huì)讓 await 跳過一些微 tick,這反而讓性能變好了。這個(gè) bug 是因?yàn)闊o意中違反了規(guī)范導(dǎo)致的,但是卻給了我們優(yōu)化的一些思路。這里我們稍微解釋下:

          const?p?=?Promise.resolve();

          (async?()?=>?{
          ??await?p;?console.log('after:await');
          })();

          p.then(()?=>?console.log('tick:a'))
          ?.then(()?=>?console.log('tick:b'));

          上面代碼一開始創(chuàng)建了一個(gè)已經(jīng)完成狀態(tài)的 promise p,然后 await 出其結(jié)果,又同時(shí)鏈了兩個(gè) then,那最終的 console.log 打印的結(jié)果會(huì)是什么呢?

          因?yàn)?p 是已完成的,你可能認(rèn)為其會(huì)先打印 'after:await',然后是剩下兩個(gè) tick, 事實(shí)上 Node.js 8 里的結(jié)果是:

          雖然以上結(jié)果符合預(yù)期,但是卻不符合規(guī)范。Node.js 10 糾正了這個(gè)行為,會(huì)先執(zhí)行 then 鏈里的,然后才是 async 函數(shù)。

          這個(gè)「正確的行為」看起來并不正常,甚至?xí)尯芏?JavaScript 開發(fā)者感到吃驚,還是有必要再詳細(xì)解釋下。在解釋之前,我們先從一些基礎(chǔ)開始。

          任務(wù)(tasks)vs. 微任務(wù)(microtasks)

          從某層面上來說,JavaScript 里存在任務(wù)和微任務(wù)。任務(wù)處理 I/O 和計(jì)時(shí)器等事件,一次只處理一個(gè)。微任務(wù)是為了 async/await 和 promise 的延遲執(zhí)行設(shè)計(jì)的,每次任務(wù)最后執(zhí)行。在返回事件循環(huán)(event loop)前,微任務(wù)的隊(duì)列會(huì)被清空。

          可以通過 Jake Archibald 的 tasks, microtasks, queues, and schedules in the browser 了解更多。Node.js 里任務(wù)模型與此非常類似。

          async 函數(shù)

          根據(jù) MDN,async 函數(shù)是一個(gè)通過異步執(zhí)行并隱式返回 promise 作為結(jié)果的函數(shù)。從開發(fā)者角度看,async 函數(shù)讓異步代碼看起來像同步代碼。

          一個(gè)最簡(jiǎn)單的 async 函數(shù):

          async?function?computeAnswer()?{
          ??return?42;
          }

          函數(shù)執(zhí)行后會(huì)返回一個(gè) promise,你可以像使用其它 promise 一樣用其返回的值。

          const?p?=?computeAnswer();
          //?→?Promise

          p.then(console.log);
          //?prints?42?on?the?next?turn

          你只能在下一個(gè)微任務(wù)執(zhí)行后才能得到 promise p 返回的值,換句話說,上面的代碼語義上等價(jià)于使用 Promise.resolve 得到的結(jié)果:

          function?computeAnswer()?{
          ??return?Promise.resolve(42);
          }

          async 函數(shù)真正強(qiáng)大的地方來源于 await 表達(dá)式,它可以讓一個(gè)函數(shù)執(zhí)行暫停直到一個(gè) promise 已接受(resolved),然后等到已完成(fulfilled)后恢復(fù)執(zhí)行。已完成的 promise 會(huì)作為 await 的值。這里的例子會(huì)解釋這個(gè)行為:

          async?function?fetchStatus(url)?{
          ??const?response?=?await?fetch(url);
          ??return?response.status;
          }

          fetchStatus 在遇到 await 時(shí)會(huì)暫停,當(dāng) fetch 這個(gè) promise 已完成后會(huì)恢復(fù)執(zhí)行,這跟直接鏈?zhǔn)教幚?fetch 返回的 promise 某種程度上等價(jià)。

          function?fetchStatus(url)?{
          ??return?fetch(url).then(response?=>?response.status);
          }

          鏈?zhǔn)教幚砗瘮?shù)里包含了之前跟在 await 后面的代碼。

          正常來說你應(yīng)該在 await 后面放一個(gè) Promise,不過其實(shí)后面可以跟任意 JavaScript 的值,如果跟的不是 promise,會(huì)被制轉(zhuǎn)為 promise,所以 await 42 效果如下:

          async?function?foo()?{
          ??const?v?=?await?42;
          ??return?v;
          }

          const?p?=?foo();
          //?→?Promise

          p.then(console.log);
          //?prints?`42`?eventually

          更有趣的是,await 后可以跟任何 “thenable”,例如任何含有 then 方法的對(duì)象,就算不是 promise 都可以。因此你可以實(shí)現(xiàn)一個(gè)有意思的 類來記錄執(zhí)行時(shí)間的消耗:

          class?Sleep?{
          ??constructor(timeout)?{
          ????this.timeout?=?timeout;
          ??}
          ??then(resolve,?reject)?{
          ????const?startTime?=?Date.now();
          ????setTimeout(()?=>?resolve(Date.now()?-?startTime),
          ???????????????this.timeout);
          ??}
          }

          (async?()?=>?{
          ??const?actualTime?=?await?new?Sleep(1000);
          ??console.log(actualTime);
          })();

          一起來看看 V8 規(guī)范 里是如何處理 await 的。下面是很簡(jiǎn)單的 async 函數(shù) foo

          async?function?foo(v)?{
          ??const?w?=?await?v;
          ??return?w;
          }

          執(zhí)行時(shí),它把參數(shù) v 封裝成一個(gè) promise ,然后會(huì)暫停直到 promise 完成,然后 w 賦值為已完成的 promise ,最后 async 返回了這個(gè)值。

          神秘的 await

          首先,V8 會(huì)把這個(gè)函數(shù)標(biāo)記為可恢復(fù)的,意味著執(zhí)行可以被暫停并恢復(fù)(從 await 角度看是這樣的)。然后,會(huì)創(chuàng)建一個(gè)所謂的 implicit_promise(用于把 async 函數(shù)里產(chǎn)生的值轉(zhuǎn)為 promise)。

          然后是有意思的東西來了:真正的 await。首先,跟在 await 后面的值被轉(zhuǎn)為 promise。然后,處理函數(shù)會(huì)綁定這個(gè) promise 用于在 promise 完成后恢復(fù)主函數(shù),此時(shí) async 函數(shù)被暫停了,返回 implicit_promise 給調(diào)用者。一旦 promise 完成了,函數(shù)會(huì)恢復(fù)并拿到從 promise 得到值 w,最后,implicit_promise 會(huì)用 w 標(biāo)記為已接受。

          簡(jiǎn)單說,await v 初始化步驟有以下組成:

          1. v 轉(zhuǎn)成一個(gè) promise(跟在 await 后面的)。
          2. 綁定處理函數(shù)用于后期恢復(fù)。
          3. 暫停 async 函數(shù)并返回 implicit_promise 給掉用者。

          我們一步步來看,假設(shè) await 后是一個(gè) promise,且最終已完成狀態(tài)的值是 42。然后,引擎會(huì)創(chuàng)建一個(gè)新的 promise 并且把 await 后的值作為 resolve 的值。借助標(biāo)準(zhǔn)里的 PromiseResolveThenableJob這些 promise 會(huì)被放到下個(gè)周期執(zhí)行。

          然后,引擎創(chuàng)建了另一個(gè)叫做 throwaway 的 promise。之所以叫這個(gè)名字,因?yàn)闆]有其它東西鏈過它,僅僅是引擎內(nèi)部用的。throwaway promise 會(huì)鏈到含有恢復(fù)處理函數(shù)的 promise 上。這里 performPromiseThen 操作其實(shí)內(nèi)部就是 Promise.prototype.then()。最終,該 async 函數(shù)會(huì)暫停,并把控制權(quán)交給調(diào)用者。

          調(diào)用者會(huì)繼續(xù)執(zhí)行,最終調(diào)用棧會(huì)清空,然后引擎會(huì)開始執(zhí)行微任務(wù):運(yùn)行之前已準(zhǔn)備就緒的 PromiseResolveThenableJob,首先是一個(gè) PromiseReactionJob,它的工作僅僅是在傳遞給 await 的值上封裝一層 promise。然后,引擎回到微任務(wù)隊(duì)列,因?yàn)樵诨氐绞录h(huán)之前微任務(wù)隊(duì)列必須要清空。

          然后是另一個(gè) PromiseReactionJob,等待我們正在 await(我們這里指的是 42)這個(gè) promise 完成,然后把這個(gè)動(dòng)作安排到 throwaway promise 里。引擎繼續(xù)回到微任務(wù)隊(duì)列,因?yàn)檫€有最后一個(gè)微任務(wù)。

          現(xiàn)在這第二個(gè) PromiseReactionJob 把決定傳達(dá)給 throwaway promise,并恢復(fù) async 函數(shù)的執(zhí)行,最后返回從 await 得到的 42。

          總結(jié)下,對(duì)于每一個(gè) await 引擎都會(huì)創(chuàng)建兩個(gè)額外的 promise(即使右值已經(jīng)是一個(gè) promise),并且需要至少三個(gè)微任務(wù)。誰會(huì)想到一個(gè)簡(jiǎn)單的 await 竟然會(huì)有如此多冗余的運(yùn)算?!

          我們來看看到底是什么引起冗余。第一行的作用是封裝一個(gè) promise,第二行為了 resolve 封裝后的 promose await 之后的值 v。這兩行產(chǎn)生個(gè)冗余的 promise 和兩個(gè)冗余的微任務(wù)。如果 v 已經(jīng)是 promise 的話就很不劃算了(大多時(shí)候確實(shí)也是如此)。在某些特殊場(chǎng)景 await42 的話,那確實(shí)還是需要封裝成 promise 的。

          因此,這里可以使用 promiseResolve 操作來處理,只有必要的時(shí)候才會(huì)進(jìn)行 promise 的封裝:

          如果入?yún)⑹?promise,則原封不動(dòng)地返回,只封裝必要的 promise。這個(gè)操作在值已經(jīng)是 promose 的情況下可以省去一個(gè)額外的 promise 和兩個(gè)微任務(wù)。此特性可以通過 --harmony-await-optimization參數(shù)在 V8(從 v7.1 開始)中開啟,同時(shí)我們 向 ECMAScript 發(fā)起了一個(gè)提案,目測(cè)很快會(huì)合并。

          下面是簡(jiǎn)化后的 await 執(zhí)行過程:

          感謝神奇的 promiseResolve,現(xiàn)在我們只需要傳 v 即可而不用關(guān)心它是什么。之后跟之前一樣,引擎會(huì)創(chuàng)建一個(gè) throwaway promise 并放到 PromiseReactionJob 里為了在下一個(gè) tick 時(shí)恢復(fù)該 async 函數(shù),它會(huì)先暫停函數(shù),把自身返回給掉用者。

          當(dāng)最后所有執(zhí)行完畢,引擎會(huì)跑微任務(wù)隊(duì)列,會(huì)執(zhí)行 PromiseReactionJob。這個(gè)任務(wù)會(huì)傳遞 promise結(jié)果給 throwaway,并且恢復(fù) async 函數(shù),從 await 拿到 42。

          盡管是內(nèi)部使用,引擎創(chuàng)建 throwaway promise 可能還是會(huì)讓人覺得哪里不對(duì)。事實(shí)證明,throwawaypromise 僅僅是為了滿足規(guī)范里 performPromiseThen 的需要。

          這是最近提議給 ECMAScript 的 變更,引擎大多數(shù)時(shí)候不再需要?jiǎng)?chuàng)建 throwaway 了。

          對(duì)比 await 在 Node.js 10 和優(yōu)化后(應(yīng)該會(huì)放到 Node.js 12 上)的表現(xiàn):

          async/await 性能超過了手寫的 promise 代碼。關(guān)鍵就是我們減少了 async 函數(shù)里一些不必要的開銷,不僅僅是 V8 引擎,其它 JavaScript 引擎都通過這個(gè) 補(bǔ)丁 實(shí)現(xiàn)了優(yōu)化。

          開發(fā)體驗(yàn)優(yōu)化

          除了性能,JavaScript 開發(fā)者也很關(guān)心問題定位和修復(fù),這在異步代碼里一直不是件容易的事。Chrome DevTools 現(xiàn)在支持了異步棧追蹤:

          在本地開發(fā)時(shí)這是個(gè)很有用的特性,不過一旦應(yīng)用部署了就沒啥用了。調(diào)試時(shí),你只能看到日志文件里的 Error#stack 信息,這些并不會(huì)包含任何異步信息。

          最近我們搞的 零成本異步棧追蹤 使得 Error#stack 包含了 async 函數(shù)的調(diào)用信息?!噶愠杀尽孤犉饋砗茏屓伺d奮,對(duì)吧?當(dāng) Chrome DevTools 功能帶來重大開銷時(shí),它如何才能實(shí)現(xiàn)零成本?舉個(gè)例子,foo 里調(diào)用 barbar 在 await 一個(gè) promise 后拋一個(gè)異常:

          async?function?foo()?{
          ??await?bar();
          ??return?42;
          }

          async?function?bar()?{
          ??await?Promise.resolve();
          ??throw?new?Error('BEEP?BEEP');
          }

          foo().catch(error?=>?console.log(error.stack));

          這段代碼在 Node.js 8 或 Node.js 10 運(yùn)行結(jié)果如下:

          $?node?index.js
          Error:?BEEP?BEEP
          ????at?bar?(index.js:8:9)
          ????at?process._tickCallback?(internal/process/next_tick.js:68:7)
          ????at?Function.Module.runMain?(internal/modules/cjs/loader.js:745:11)
          ????at?startup?(internal/bootstrap/node.js:266:19)
          ????at?bootstrapNodeJSCore?(internal/bootstrap/node.js:595:3)

          注意到,盡管是 foo() 里的調(diào)用拋的錯(cuò),foo 本身卻不在棧追蹤信息里。如果應(yīng)用是部署在云容器里,這會(huì)讓開發(fā)者很難去定位問題。

          有意思的是,引擎是知道 bar 結(jié)束后應(yīng)該繼續(xù)執(zhí)行什么的:即 foo 函數(shù)里 await 后。恰好,這里也正是 foo 暫停的地方。引擎可以利用這些信息重建異步的棧追蹤信息。有了以上優(yōu)化,輸出就會(huì)變成這樣:

          $?node?--async-stack-traces?index.js
          Error:?BEEP?BEEP
          ????at?bar?(index.js:8:9)
          ????at?process._tickCallback?(internal/process/next_tick.js:68:7)
          ????at?Function.Module.runMain?(internal/modules/cjs/loader.js:745:11)
          ????at?startup?(internal/bootstrap/node.js:266:19)
          ????at?bootstrapNodeJSCore?(internal/bootstrap/node.js:595:3)
          ????at?async?foo?(index.js:2:3)

          在棧追蹤信息里,最上層的函數(shù)出現(xiàn)在第一個(gè),之后是一些異步調(diào)用棧,再后面是 foo 里面 bar 上下文的棧信息。這個(gè)特性的啟用可以通過 V8 的 --async-stack-traces 參數(shù)啟用。

          然而,如果你跟上面 Chrome DevTools 里的棧信息對(duì)比,你會(huì)發(fā)現(xiàn)棧追蹤里異步部分缺失了 foo 的調(diào)用點(diǎn)信息。這里利用了 await 恢復(fù)和暫停位置是一樣的特性,但 Promise#then() 或 Promise#catch()就不是這樣的??梢钥?Mathias Bynens 的文章 await beats Promise#then() 了解更多。

          結(jié)論

          async 函數(shù)變快少不了以下兩個(gè)優(yōu)化:

          • 移除了額外的兩個(gè)微任務(wù)
          • 移除了 throwaway promise

          除此之外,我們通過 零成本異步棧追蹤 提升了 awaitPromise.all() 開發(fā)調(diào)試體驗(yàn)。

          我們還有些對(duì) JavaScript 開發(fā)者友好的性能建議:

          多使用 asyncawait 而不是手寫 promise 代碼,多使用 JavaScript 引擎提供的 promise 而不是自己去實(shí)現(xiàn)。

          非常歡迎有激情的你加入 ES2049 Studio,簡(jiǎn)歷請(qǐng)發(fā)送至 caijun.hcj(at)alibaba-inc.com 。

          瀏覽 49
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久热小视频 | 五月天成人综合 | 天天射天天日天天 | 性欧美成人播放77777 | 中文字幕欧美在线 |