<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    異步編程的終極解決方案 async/await:用同步的方式去寫異步代碼

    共 8775字,需瀏覽 18分鐘

     ·

    2021-07-07 17:18

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

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

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

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

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

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

    比如,如果多個回調(diào)存在依賴,可能會寫成:

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

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

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

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

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

    過渡方案 Generator

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

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

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

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

    • 在 Generator 函數(shù)內(nèi)部執(zhí)行一段代碼,如果遇到 yield 關(guān)鍵字,那么 JS 引擎將返回關(guān)鍵字后面的內(nèi)容給外部,并暫停該函數(shù)的執(zhí)行。
    • 外部函數(shù)可以通過 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ù)不會像普通函數(shù)一樣立即執(zhí)行,而是 返回一個指向內(nèi)部狀態(tài)對象的指針,所以要調(diào)用遍歷器對象 Iterator 的 next 方法,指針就會從函數(shù)頭部或者上一次停下來的地方開始執(zhí)行。

    如下:

    image.png

    next 方法:

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

    Generator 生成器也是通過同步的方式寫異步代碼的,也可以解決回調(diào)地獄的問題,但是比較難以理解,希望下面的例子能夠幫助你理解 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 不傳參時,yield 返回 undefined

    如下圖:

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

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

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

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

    如下圖:

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

    協(xié)程

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

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

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

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

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

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

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

      console.log("開始執(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í)行過程如下圖所示,可以重點(diǎn)關(guān)注協(xié)程之間的切換:

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

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

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

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

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

    成熟方案 Promise

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

    終極解決方案 async/await

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

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

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

    async

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

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

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

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

    await

    await 需要跟 async 搭配使用,結(jié)合下面這段代碼來看看 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é)程 的視角來看看這段代碼的整體執(zhí)行流程圖:

    image.png

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

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

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

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

    async/await總結(jié)

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

    異步編程總結(jié)

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

    關(guān)于本文

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

    https://juejin.cn/post/6978689182809997320

    最后

    歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
    回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認(rèn)真的解答喲!
    回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
    回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
    如果這篇文章對你有幫助,在看」是最大的支持
     》》面試官也在看的算法資料《《
    “在看和轉(zhuǎn)發(fā)”就是最大的支持
    瀏覽 38
    點(diǎn)贊
    評論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報
    評論
    圖片
    表情
    推薦
    點(diǎn)贊
    評論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    日韩AV电影免费观看 | 亚洲日韩字幕 | 视频一区在线看 | 啪啪啪啪啪AV | 国产精品日韩在线 |