避免地獄async?await的使用及原理解析

 更新時間:2022年07月04日 16:59:01   作者:智影Yodonicc  
這篇文章主要為大家介紹了避免地獄async?await的使用場景及原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

無論你對async/await的立場如何,我都想向你說明,根據我的經驗,為什么async/await往往會使代碼復雜度更高,而不是更低。

JavaScript中的async/await功能的效用是基于這樣的想法:異步代碼很難,相比之下,同步代碼更容易。這在客觀上是正確的,但在大多數情況下,我不認為async/await真的能解決這個問題。

謊言和async/await

我用來確定是否要使用某個模式的指標之一是它所帶來的代碼綜合質量。例如,一個模式可能是干凈的、簡潔的或廣泛使用的,但如果它導致了容易出錯的代碼,它就是一個我可能會拒絕的模式。這些模式是雙刃劍,很容易搬起石頭砸自己的腳。

首先,它是建立在一個謊言之上的。

Async/await讓你的異步代碼看起來像同步的一樣。

這是它的賣點。但對我來說,這就是問題所在。它從一開始就為你的代碼所發生的事情設定了錯誤的心理模型。同步代碼可能比異步代碼更容易處理,但同步代碼不是異步代碼。它們有非常不同的屬性。

很多時候這不是問題,但當它是問題時,就很難識別,因為async/await正好隱藏了顯示它的線索。以這段代碼為例。

同步代碼

const processData = ({ userData, sessionPrefences }) => {
  save('userData', userData);
  save('session', sessionPrefences);
  return { userData, sessionPrefences }
}

Async/Await

const processData = async ({ userData, sessionPrefences }) => {
  await save('userData', userData);
  await save('session', sessionPrefences);
  return { userData, sessionPrefences }
}

Promise

const processData = ({ userData, sessionPrefences }) =>   save('userData', userData)
  .then(() => save('session', sessionPrefences))
  .then(() => ({ userData, sessionPrefences })

這里有一些性能問題。我們已經把問題縮小到了processData函數上。在這三種情況中,你對優化途徑的假設是什么?

我看了第一種情況,發現我們在兩個不同的地方保存了兩塊不同的數據,然后只是返回一個對象。唯一可以優化的地方是保存函數。沒有任何其他選擇。

我看了第二個例子,也有同樣的想法。唯一可以優化的地方是保存函數。

也許只是因為我對Promise的太熟悉了,但我看了第三個例子,我很快看到了一個機會。我看到我們在連續調用save,盡管其中一個并不依賴于另一個。 我們可以將我們的兩個save調用并行化。

const processData = ({ userData, sessionPrefences }) => Promise.all([
  save('userData', userData),
  save('session', sessionPrefences)
])
  .then(() => ({ userData, sessionPrefences })

同樣的機會也存在于async/await代碼中,只是因為我們處于異步代碼的思維模式中,所以它被隱藏在明處。在async/await版本中并不是沒有提示。關鍵字async和await應該給我們同樣的直覺,就像第三個版本中的then一樣。但我敢打賭,對許多工程師來說,它并沒有。

為什么沒有呢?

這是因為我們被教導要以同步的思維方式來閱讀async/await代碼。在第一個同步代碼例子中,我們無法將保存調用并行化,同樣的邏輯(但現在是不正確的),我們來到第二個例子。Async/await將我們的思維置于同步的思維模式中,而這是錯誤的思維模式。

此外,如果我們要在async/await的例子中利用并行化的優勢,無論如何我們必須使用promise。

const processData = async ({ userData, sessionPrefences }) => {
  await Promise.all([
    save('userData', userData), 
    save('session',sessionPrefences)
  ])
  return { userData, sessionPrefences }
}

在我看來,如果一個特定的模式只在一些常見的情況下工作,那么它一定有一些非常大的優勢。如果我不得不在一些非常常見的情況下 "退回"到promise模式,那么我就看不到async/await比promise有什么優勢。對我來說,在多種范式之間切換的認知負擔并不值得。promise在任何情況下都能完成工作,而且每次都和async/await一樣好,甚至更好。

錯誤處理

處理錯誤對于異步代碼來說是至關重要的。有幾個關鍵的地方,我們必須擔心JavaScript中同步代碼的錯誤處理。這主要發生在我們把一些東西交給本地API,如JSON.parse,或瀏覽器功能,如window.localstorage。

讓我們來看看我們之前的save函數的例子,并應用一些錯誤處理。讓我們假設在我們的同步例子中,save執行了一個可能會拋出的操作。這是非常合理的,因為如果保存到sessionstorage,它可能在序列化或試圖訪問sessionstorage的過程中拋出。為了處理同步代碼中可能出現的錯誤,我們通常使用try/catch。

同步代碼

const processData = ({ userData, sessionPrefences }) => {
  try {
    save('userData', userData);
    save('session', sessionPrefences);
    return { userData, sessionPrefences }
  } catch (err) {
    handleErrorSomehow(err)
  }
}

根據不同的策略,我們可能重新拋出錯誤,或者在catch塊中返回一些默認值。無論哪種方式,我們都必須在try塊中封裝任何可能拋出錯誤的邏輯。

async/await

由于async/await讓我們 "像看待同步一樣看待async代碼",我們也使用try/catch塊。捕獲塊甚至會將我們的reject判定為一個錯誤。

const processData = async ({ userData, sessionPrefences }) => {
  try {
    await save('userData', userData);
    await save('session', sessionPrefences);
    return { userData, sessionPrefences }
  } catch (err) {
    handleErrorSomehow(err)
  }
}

看看這個,async/await實現了它的承諾。它看起來與同步版本幾乎完全一樣。

現在,有一些編程流派非常倚重try/catches。我覺得它們是一種精神上的負擔。每當有try/catch時,我們現在不僅要擔心函數返回什么,還要擔心它拋出什么。我們不僅有分支邏輯,這增加了復雜性,而且還必須擔心同時處理兩種不同的范式。一個函數可以返回一個值,也可以拋出。因此,每個函數都要處理這兩方面的問題。這很累人。

try/catch的尷尬

關于try/catch的最后一點。在JavaScript中,你一般不會在很多地方看到擁抱try/catch。與其他語言不同的是,在其他語言中,你會經??吹剿?,比如Java。JavaScript中的try塊會立即將這部分代碼排除在許多引擎優化之外,因為代碼不能再被分解成確定的片段。換句話說,在JavaScript中,同樣的代碼在被try塊包裹的情況下會比不被包裹的情況下運行得更慢,即使它沒有拋出的可能性。

Promise

讓我們看看Promise在做什么。

const processData = ({ userData, sessionPrefences }) =>   save('userData', userData)
  .then(() => save('session', sessionPrefences))
  .then(() => ({ userData, sessionPrefences })
  .catch(handleErrorSomehow)

你看到了嗎?

.catch(handleErrorSomehow)

是的。這就是它的全部內容。這和其他的方法做的事情完全一樣。我發現這比try/catch塊更容易閱讀。你覺得呢?如果同步代碼也這么簡單就好了......等一下!

const processData = ({ userData, sessionPrefences }) => Promise.resolve(save('userData', userData))
  .then(() => save('session', sessionPrefences))
  .then(() => ({ userData, sessionPrefences })
  .catch(handleErrorSomehow)

好吧,這有缺點,但也超級有趣,你不覺得嗎?這只是一個小小的提示,讓你思考如果我們想的話,函數式風格的JavaScript會是什么樣子。但不管怎樣,接受還是不接受。我的目的是說服你使用Promises而不是async/await。而不是承諾Promises全面優于async/await。那就太瘋狂了。??

更關鍵的一點

我想提出的最后一點是。我有時會遇到一些論點,聲稱async/await可以防止callbacks和promises中可能出現的 "回調地獄 "現象。

說實話,我第一次聽到這種論調時,我以為這個人只是混淆了,是想說 "callbacks"。畢竟,promises設計之初的目的之一就是消除 "回調地獄 "的問題,所以我很困惑,人們說promises會導致回調地獄(我的意思是,它畢竟被稱為回調(callbacks)地獄,而不是promises地獄)。

但后來我真的看到了一些promise的代碼,它們看起來驚人地像回調地獄。我很困惑,為什么有人會這樣使用promise。最終,我得出結論,有些人對promise的工作原理有一個非?;镜恼`解。

在我討論這個問題之前,首先讓我承認,事實上不可能用async/await創造出金字塔結構的回調地獄,所以它有這個優勢。但是我從來沒有寫過一個超過兩級的promise流,沒有必要。當然有可能,但從來沒有必要。

我發現,每當我在promise鏈中看到 "回調地獄 "時,都是因為人們沒有意識到promise的作用就像一個無限長的流程圖。換句話說,一個像這樣的流程:

const id = 5
const lotsOAsync = () => fetch('/big-o-list')
  .then((result) => {
    if (result.ok) {
      return result.json().then((list) => {
        const {url: itemURL } = data.items.find((item) => item.id === id)
        return fetch(itemURL).then((result) => {
          if (result.ok) {
            return result.json().then((data) => data.name)
          } else {
            throw new Error(`Couldn't fetch ${itemURL}`)
          }
        })
      })
    } else {
      throw new Error(`Couldn't fetch big-o-list`)
    }
  })

真的應該這樣寫:

const id = 5
const lotsOAsync = () => fetch('/big-o-list')
  .then((result) => result.ok ? result.json() : Promise.reject(`Couldn't fetch big-o-list`))
  .then(({ items }) => items.find((item) => item.id === id))
  .then(({url}) => fetch(url))
  .then((result) => result.ok ? result.json() : Promise.reject(`Couldn't fetch ${result.request.url}`))
  .then((data) => data.name)

如果這有點讓人困惑,讓我給你一個更簡單,但更矯情的例子。

回調地獄 ??

Promise.resolve(
  Promise.resolve(
    Promise.resolve(
      Promise.resolve(
        Promise.resolve(
          Promise.resolve(5)
        )
      )
    )
  )
).then((val) => console.log(val))

promise天堂 ??

Promise.resolve()
  .then(() => Promise.resolve())
  .then(() => Promise.resolve())
  .then(() => Promise.resolve())
  .then(() => Promise.resolve())
  .then(() => Promise.resolve(5))
  .then((val) => console.log(val))

這兩個例子在創建promise的數量和順序方面都是一樣的(以及它們實際上脫離現實,但這是出于學術目的,所以我們允許它)。然而,后一個比前一個更有可讀性。

如果你習慣于寫與第一個例子更像的promise流,讓我給你提供一個好的小技巧來擺脫這種習慣。

每次你想在你的承諾流中寫一個then或catch,首先確保你返回promise,然后轉到最外層的promise(如果你一直遵循這個規則,那應該只有一層)并在那里添加你的then或catch。只要你在返回,你的值就會冒泡到最外層的promise。這就是你應該做的 "then"。

請記住,你不一定要返回一個Promise來使用then。一旦你在一個promise的上下文中,任何返回的值都會通過它冒泡。Promise、number、字符串、函數、對象,等等。

Promise.resolve(5)
  .then((val) => val + 3)
  .then((num) => String(num))
  .then((str) => `I have ${str} llamas`)
  .then((str) => /ll/.test(str))

這都是完全合法的(盡管有點不合理,但編造的例子是為了說清楚??♂)

本文參考Why I avoid async/await,有修改。

總結

在我看來,promises

  • 提供更好的提示,表明我們處于異步的心理模型中
  • 在簡單的情況下,對代碼的表達至少與async/await一樣干凈。
  • 為包括錯誤處理和并行化在內的更復雜的工作流提供了一個更干凈的選擇。

注:特別感謝技術指導dazhao(趙達)對本文的審閱指正。

以上就是避免地獄async await的使用及原理解析的詳細內容,更多關于避免地獄async await原理的資料請關注腳本之家其它相關文章!

相關文章

  • JavaScript實現隊列結構過程

    JavaScript實現隊列結構過程

    這篇文章主要介紹了JavaScript實現隊列結構的過程,隊列即Queue,它是受限的線性表,先進先出,受限之處在于它只允許在表的前端進行刪除操作,下面我們一起進入文章學習更加詳細內容,需要的朋友也可以參考一下
    2021-12-12
  • 微信小程序與php 實現微信支付的簡單實例

    微信小程序與php 實現微信支付的簡單實例

    這篇文章主要介紹了微信小程序與php 實現微信支付的簡單實例的相關資料,需要的朋友可以參考下
    2017-06-06
  • 制作特殊字的腳本

    制作特殊字的腳本

    制作特殊字的腳本...
    2006-06-06
  • 微信小程序登錄態控制深入分析

    微信小程序登錄態控制深入分析

    這篇文章主要介紹了微信小程序登錄態控制深入分析的相關資料,需要的朋友可以參考下
    2017-04-04
  • Javascript實現的分頁函數

    Javascript實現的分頁函數

    Javascript實現的分頁函數...
    2006-12-12
  • 微信小程序中的onLoad詳解及簡單實例

    微信小程序中的onLoad詳解及簡單實例

    這篇文章主要介紹了微信小程序中的onLoad詳解及簡單實例的相關資料,需要的朋友可以參考下
    2017-04-04
  • 微信小程序 modal組件詳細介紹

    微信小程序 modal組件詳細介紹

    這篇文章主要介紹了微信小程序 modal組件詳細介紹的相關資料,需要的朋友可以參考下
    2016-09-09
  • 微信小程序 限制1M的瘦身技巧與方法詳解

    微信小程序 限制1M的瘦身技巧與方法詳解

    這篇文章主要介紹了微信小程序 瘦身技巧與方法詳解的相關資料,現在微信小程序 提交代碼只有1M大小的限制,這就對開發者提出難題了,這里提供了相關方法技巧,需要的朋友可以參考下
    2017-01-01
  • JavaScript實現優先級隊列

    JavaScript實現優先級隊列

    這篇文章主要介紹了JavaScript如何實現優先級隊列,在計算機里,隊列是一種先進先出的數據結構。就跟我們平時排隊一樣,先到的排在前面,前面的優先處理,下面我們就來看看在JavaScript里面的優先隊列又當如何
    2021-12-12
  • Javascript 常見的高階函數詳情

    Javascript 常見的高階函數詳情

    這篇文章主要介紹了Javascript 常見的高階函數的相關資料,高階函數,英文叫 Higher Order function。一個函數可以接收另外一個函數作為參數,這種函數就叫做高階函數,需要的朋友可以參考一下
    2021-09-09

最新評論

美丽人妻被按摩中出中文字幕