<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/await:用同步的方式去寫(xiě)異步代碼

          共 8775字,需瀏覽 18分鐘

           ·

          2021-07-07 17:18

          點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)

          回復(fù)算法,加入前端編程面試算法每日一題群

          早期的回調(diào)函數(shù)

          回調(diào)函數(shù)我們經(jīng)常有寫(xiě)到,比如:

          ajax(url, (res) => {
            console.log(res);
          })
          復(fù)制代碼

          但是這種回調(diào)函數(shù)有一個(gè)大缺陷,就是會(huì)寫(xiě)出 回調(diào)地獄(Callback hell

          比如,如果多個(gè)回調(diào)存在依賴,可能會(huì)寫(xiě)成:

          ajax(url, (res) => {
            console.log(res);
            // ...處理代碼
            ajax(url2, (res2) => {
              console.log(res2);
              // ...處理代碼
              ajax(url3, (res3) => {
                console.log(res3);
                // ...處理代碼
              })
            })
          })
          復(fù)制代碼

          這個(gè)就是 回調(diào)地獄

          • 內(nèi)嵌函數(shù)存在耦合,牽一發(fā)而動(dòng)全身,改一個(gè)會(huì)影響其它地方
          • 內(nèi)嵌函數(shù)多了,發(fā)生錯(cuò)誤要怎么處理呢?這是一個(gè)難題

          早期回調(diào)函數(shù)的優(yōu)缺點(diǎn):

          • 優(yōu)點(diǎn):解決了 同步阻塞 的問(wèn)題(只要有一個(gè)任務(wù)耗時(shí)很長(zhǎng),后面的任務(wù)都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行)
          • 缺點(diǎn):回調(diào)地獄;不能用 try catch 捕獲錯(cuò)誤;不能 return

          過(guò)渡方案 Generator

          ES6 新引入了 Generator 函數(shù)(生成器函數(shù)),可以通過(guò) yield 關(guān)鍵字,把函數(shù)的執(zhí)行流掛起,為改變執(zhí)行流程提供了可能,從而為異步編程提供解決方案。最大的特點(diǎn)就是 可以控制函數(shù)的執(zhí)行。

          Generator 有兩個(gè)區(qū)分于普通函數(shù)的部分:

          • 一是在 function后面,函數(shù)名之前有個(gè) *,用來(lái)表示函數(shù)為 Generator 函數(shù)
          • 函數(shù)內(nèi)部有 yield 表達(dá)式,用來(lái)定義函數(shù)內(nèi)部的狀態(tài)

          Generator 函數(shù)的具體使用方式是:

          • 在 Generator 函數(shù)內(nèi)部執(zhí)行一段代碼,如果遇到 yield 關(guān)鍵字,那么 JS 引擎將返回關(guān)鍵字后面的內(nèi)容給外部,并暫停該函數(shù)的執(zhí)行。
          • 外部函數(shù)可以通過(guò) next 方法恢復(fù)函數(shù)的執(zhí)行。
          functionfn() {
            console.log("one");
            yield '1';
            console.log("two");
            yield '2';
            console.log("three");
            return '3';
          }
          復(fù)制代碼

          調(diào)用 Generator 函數(shù)和調(diào)用普通函數(shù)一樣,在函數(shù)名后面加上 () 即可,但是 Generator 函數(shù)不會(huì)像普通函數(shù)一樣立即執(zhí)行,而是 返回一個(gè)指向內(nèi)部狀態(tài)對(duì)象的指針,所以要調(diào)用遍歷器對(duì)象 Iterator 的 next 方法,指針就會(huì)從函數(shù)頭部或者上一次停下來(lái)的地方開(kāi)始執(zhí)行。

          如下:

          image.png

          next 方法:

          一般情況下, next 方法不傳入?yún)?shù)的時(shí)候,yield 表達(dá)式的返回值是 undefined。當(dāng) next 傳入?yún)?shù)的時(shí)候,該參數(shù)會(huì)作為上一步 yield 的返回值。

          Generator 生成器也是通過(guò)同步的方式寫(xiě)異步代碼的,也可以解決回調(diào)地獄的問(wèn)題,但是比較難以理解,希望下面的例子能夠幫助你理解 Generator 生成器:

          functionsum(a{
            console.log('a:', a);
            let b = yield 1;
            console.log('b:', b);
            let c = yield 2;
            console.log('c:', c);
            let sum = a + b + c;
            console.log('sum:', sum)
            return sum;
          }
          復(fù)制代碼
          • next 不傳參時(shí),yield 返回 undefined

          如下圖:

          image.png
          • 當(dāng)?shù)谝淮螆?zhí)行 next 時(shí),傳參會(huì)被忽略,并且函數(shù)暫停在 yield 1 處,所以返回 1

          • 當(dāng)?shù)诙螆?zhí)行 next 時(shí),不傳參,那么 yield 1 返回的是 undefined ,所以 b 的值是 undefined

          • 第三次同理,c 的值為 undefined

          • 當(dāng) next 傳入?yún)?shù)時(shí),該參數(shù)會(huì)作為上一步 yield 的返回值

          如下圖:

          image.png
          • 當(dāng)?shù)谝淮螆?zhí)行 next 時(shí),傳參(20)會(huì)被忽略,并且函數(shù)暫停在 yield 1 處,所以返回 1
          • 當(dāng)?shù)诙螆?zhí)行 next 時(shí),傳參 30,作為 yield 1 返回的值,所以 b = yield 1,b 的值是 30
          • 當(dāng)?shù)诙螆?zhí)行 next 時(shí),傳參 40,作為 yield 2 返回的值,所以 c = yield 2, c 的值是 40

          協(xié)程

          我們知道,async/await 是一個(gè)自動(dòng)執(zhí)行的 Generator 函數(shù),上面已經(jīng)介紹了 Generator 函數(shù),那么接下來(lái)很有必要介紹一下 V8 引擎是如何實(shí)現(xiàn)一個(gè)函數(shù)的暫停和恢復(fù) 的呢?

          要搞懂函數(shù)為何能暫停和恢復(fù),首先要了解 協(xié)程 的概念。進(jìn)程和線程我們都知道,那么協(xié)程是什么呢?

          協(xié)程是一種比線程更加輕量級(jí)的存在??梢园褏f(xié)程看成是跑在線程上的任務(wù),一個(gè)線程上可以存在多個(gè)協(xié)程,但是在線程上同時(shí)只能執(zhí)行一個(gè)協(xié)程,比如當(dāng)前執(zhí)行的是 A 協(xié)程,要啟動(dòng) B 協(xié)程,那么 A 協(xié)程就需要將主線程的控制權(quán)交給 B 協(xié)程,這就體現(xiàn)在 A 協(xié)程暫停執(zhí)行,B 協(xié)程恢復(fù)執(zhí)行;同樣,也可以從 B 協(xié)程中啟動(dòng) A 協(xié)程。通常,如果從 A 協(xié)程啟動(dòng) B 協(xié)程,我們就把 A 協(xié)程稱為 B 協(xié)程的父協(xié)程。

          正如一個(gè)進(jìn)程可以擁有多個(gè)線程一樣,一個(gè)線程也可以擁有多個(gè)協(xié)程。最重要的是,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而是完全由程序所控制(即在用戶態(tài)執(zhí)行)。這樣帶來(lái)的好處就是性能得到了很大的提升,不會(huì)像線程切換那樣消耗資源。

          可以結(jié)合代碼理解:

          functiongenDemo() {
            console.log("開(kāi)始執(zhí)行第一段")
            yield 'generator 2'

            console.log("開(kāi)始執(zhí)行第二段")
            yield 'generator 2'

            console.log("開(kāi)始執(zhí)行第三段")
            yield 'generator 2'

            console.log("執(zhí)行結(jié)束")
            return 'generator 2'
          }

          console.log('main 0')
          let gen = genDemo()
          console.log(gen.next().value)
          console.log('main 1')
          console.log(gen.next().value)
          console.log('main 2')
          console.log(gen.next().value)
          console.log('main 3')
          console.log(gen.next().value)
          console.log('main 4')
          復(fù)制代碼

          執(zhí)行過(guò)程如下圖所示,可以重點(diǎn)關(guān)注協(xié)程之間的切換:

          從圖中可以看出來(lái)協(xié)程的四點(diǎn)規(guī)則:

          • 通過(guò)調(diào)用生成器函數(shù) genDemo 來(lái)創(chuàng)建一個(gè) 協(xié)程 gen,創(chuàng)建之后,gen 協(xié)程并沒(méi)有立即執(zhí)行。
          • 要讓 gen 協(xié)程執(zhí)行,需要通過(guò)調(diào)用 gen.next。
          • 當(dāng)協(xié)程正在執(zhí)行的時(shí)候,可以 通過(guò) yield 關(guān)鍵字來(lái)暫停 gen 協(xié)程的執(zhí)行,并返回主要信息給父協(xié)程。
          • 如果協(xié)程在執(zhí)行期間,遇到了 return 關(guān)鍵字,那么 JS 引擎會(huì)結(jié)束當(dāng)前協(xié)程,并將 return 后面的內(nèi)容返回給父協(xié)程。

          協(xié)程之間的切換:

          • gen 協(xié)程和父協(xié)程是在主線程上交互執(zhí)行的,并不是并發(fā)執(zhí)行的,它們之前的切換是 通過(guò) yield 和 gen.next 來(lái)配合完成 的。
          • 當(dāng)在 gen 協(xié)程中調(diào)用了 yield 方法時(shí),JS 引擎會(huì)保存 gen 協(xié)程當(dāng)前的調(diào)用棧信息,并恢復(fù)父協(xié)程的調(diào)用棧信息。同樣,當(dāng)在父協(xié)程中執(zhí)行 gen.next 時(shí),JS 引擎會(huì)保存父協(xié)程的調(diào)用棧信息,并恢復(fù) gen 協(xié)程的調(diào)用棧信息。
          image.png

          其實(shí)在 JS 中,Generator 生成器就是協(xié)程的一種實(shí)現(xiàn)方式。

          成熟方案 Promise

          關(guān)于 Promise,可以去看我上一篇文章:《異步編程 Promise:從使用到手寫(xiě)實(shí)現(xiàn)(4200字長(zhǎng)文)》,在這一篇文章中詳細(xì)介紹了 Promise 如何解決回調(diào)地獄的問(wèn)題,了解 Promise 和微任務(wù)的淵源,然后帶你一步一步的解構(gòu)手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Promise,最后簡(jiǎn)單介紹并手寫(xiě)實(shí)現(xiàn)了一些 Promise 的 API,包括 Promise.all、Promise.allSettled、Promise.racePromise.finally 等API。

          終極解決方案 async/await

          使用 Promise 能很好地解決回調(diào)地獄的問(wèn)題,但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較復(fù)雜的話,那么整段代碼將充斥著 then,語(yǔ)義化不明顯,代碼不能很好地表示執(zhí)行流程。

          基于這個(gè)原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個(gè)重大改進(jìn),提供了 在不阻塞主線程的情況下使用同步代碼實(shí)現(xiàn)異步訪問(wèn)資源的能力,并且使得代碼邏輯更加清晰。

          其實(shí) async/await 技術(shù)背后的秘密就是 Promise 和 Generator 生成器應(yīng)用,往低層說(shuō)就是 微任務(wù)和協(xié)程應(yīng)用。要搞清楚 async 和 await 的工作原理,我們得對(duì) async 和 await 分開(kāi)分析。

          async

          async 到底是什么?根據(jù) MDN 定義,async 是一個(gè)通過(guò) 異步執(zhí)行并隱式返回 Promise 作為結(jié)果的函數(shù)。重點(diǎn)關(guān)注兩個(gè)詞:異步執(zhí)行和隱式返回 Promise。

          先來(lái)看看是如何隱式返回 Promise 的,參考下面的代碼:

          async function async1() {
            return '秀兒';
          }
          console.log(async1()); // Promise {<fulfilled>: "秀兒"}
          復(fù)制代碼

          執(zhí)行這段代碼,可以看到調(diào)用 async 聲明的 async1 函數(shù)返回了一個(gè) Promise 對(duì)象,狀態(tài)是 resolved,返回結(jié)果如下所示:Promise {<fulfilled>: "秀兒"}。和 Promise 的鏈?zhǔn)秸{(diào)用 then 中處理返回值一樣。

          await

          await 需要跟 async 搭配使用,結(jié)合下面這段代碼來(lái)看看 await 到底是什么:

          async function foo() {
            console.log(1)
            let a = await 100
            console.log(a)
            console.log(2)
          }
          console.log(0)
          foo()
          console.log(3)
          復(fù)制代碼

          站在 協(xié)程 的視角來(lái)看看這段代碼的整體執(zhí)行流程圖:

          image.png

          結(jié)合上圖來(lái)分析 async/await 的執(zhí)行流程:

          • 首先,執(zhí)行 console.log(0) 這個(gè)語(yǔ)句,打印出來(lái) 0。
          • 緊接著就是執(zhí)行 foo 函數(shù),由于 foo 函數(shù)是被 async 標(biāo)記過(guò)的,所以當(dāng)進(jìn)入該函數(shù)的時(shí)候,JS 引擎會(huì)保存當(dāng)前的調(diào)用棧等信息,然后執(zhí)行 foo 函數(shù)中的 console.log(1) 語(yǔ)句,并打印出 1。
          • 當(dāng)執(zhí)行到 await 100 時(shí),會(huì)默認(rèn)創(chuàng)建一個(gè) Promise 對(duì)象
          • 代碼如下所示:let promise_ = new Promise((resolve,reject){ resolve(100) })
          • 在這個(gè) promise_ 對(duì)象創(chuàng)建的過(guò)程中,可以看到在 executor 函數(shù)中調(diào)用了 resolve 函數(shù),JS 引擎會(huì)將該任務(wù)提交給微任務(wù)隊(duì)列。
          • 然后 JS 引擎會(huì)暫停當(dāng)前協(xié)程的執(zhí)行,將主線程的控制權(quán)轉(zhuǎn)交給父協(xié)程執(zhí)行,同時(shí)會(huì)將 promise_ 對(duì)象返回給父協(xié)程。
          • 主線程的控制權(quán)已經(jīng)交給父協(xié)程了,這時(shí)候父協(xié)程要做的一件事是調(diào)用 promise_.then 來(lái)監(jiān)控 promise 狀態(tài)的改變。
          • 接下來(lái)繼續(xù)執(zhí)行父協(xié)程的流程,執(zhí)行 console.log(3),并打印出來(lái) 3。
          • 隨后父協(xié)程將執(zhí)行結(jié)束,在結(jié)束之前,會(huì)進(jìn)入微任務(wù)的檢查點(diǎn),然后執(zhí)行微任務(wù)隊(duì)列,微任務(wù)隊(duì)列中有 resolve(100) 的任務(wù)等待執(zhí)行,執(zhí)行到這里的時(shí)候,會(huì)觸發(fā) promise_.then 中的回調(diào)函數(shù),如下所示:
          promise_.then((value) => {
            // 回調(diào)函數(shù)被激活后
            // 將主線程控制權(quán)交給foo協(xié)程,并將vaule值傳給協(xié)程
          })
          復(fù)制代碼
          • 該回調(diào)函數(shù)被激活以后,會(huì)將主線程的控制權(quán)交給 foo 函數(shù)的協(xié)程,并同時(shí)將 value 值傳給該協(xié)程。
          • foo 協(xié)程激活之后,會(huì)把剛才的 value 值賦給了變量 a,然后 foo 協(xié)程繼續(xù)執(zhí)行后續(xù)語(yǔ)句,執(zhí)行完成之后,將控制權(quán)歸還給父協(xié)程。

          以上就是 await/async 的執(zhí)行流程。正是因?yàn)?nbsp;async 和 await 在背后做了大量的工作,所以我們才能用同步的方式寫(xiě)出異步代碼來(lái)。

          當(dāng)然也存在一些缺點(diǎn),因?yàn)?nbsp;await 將異步代碼改造成了同步代碼,如果多個(gè)異步代碼沒(méi)有依賴性卻使用了 await 會(huì)導(dǎo)致性能上的降低。

          async/await總結(jié)

          • Promise 的編程模型依然充斥著大量的 then 方法,雖然解決了回調(diào)地獄的問(wèn)題,但是在語(yǔ)義方面依然存在缺陷,代碼中充斥著大量的 then 函數(shù),這就是 async/await 出現(xiàn)的原因。
          • 使用 async/await 可以實(shí)現(xiàn)用同步代碼的風(fēng)格來(lái)編寫(xiě)異步代碼,這是因?yàn)?nbsp;async/await 的基礎(chǔ)技術(shù)使用了 Generator 生成器和 Promise,Generator 生成器是協(xié)程的實(shí)現(xiàn),利用 Generator 生成器能實(shí)現(xiàn)生成器函數(shù)的暫停和恢復(fù)。
          • 另外,V8 引擎還為 async/await 做了大量的語(yǔ)法層面包裝,所以了解隱藏在背后的代碼有助于加深你對(duì) async/await 的理解。
          • async/await 無(wú)疑是異步編程領(lǐng)域非常大的一個(gè)革新,也是未來(lái)的一個(gè)主流的編程風(fēng)格。其實(shí),除了 JavaScript,Python、Dart、C# 等語(yǔ)言也都引入了 async/await,使用它不僅能讓代碼更加整潔美觀,而且還能確保該函數(shù)始終都能返回 Promise。

          異步編程總結(jié)

          • 早期的異步回調(diào)函數(shù)雖然解決了同步阻塞的問(wèn)題,但是容易寫(xiě)出回調(diào)地獄。
          • Generator 生成器最大的特點(diǎn)是可以控制函數(shù)的執(zhí)行,是協(xié)程的一種實(shí)現(xiàn)方式。
          • Promise 的更多內(nèi)容可以看我的這篇文章:《異步編程 Promise:從使用到手寫(xiě)實(shí)現(xiàn)(4200字長(zhǎng)文)》:https://juejin.cn/post/6978419919582920740
          • async/await 可以算是異步編程的終極解決方案,它通過(guò)同步的方式寫(xiě)異步代碼,可以把 await 看作是讓出線程的標(biāo)志,先去執(zhí)行 async 函數(shù)外部的代碼,等調(diào)用棧為空再回來(lái)調(diào)用 await 后面的代碼。

          關(guān)于本文

          來(lái)源:起風(fēng)了Q

          https://juejin.cn/post/6978689182809997320

          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對(duì)你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持
          瀏覽 38
          點(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>
                  国产爱操| 欧洲亚洲韩国在线观看 | 熟女一区二区三区视频 | 丁香六月天| 波多野结衣中文字幕一区 |