javascript異步中的回調

同期異步系列文章推薦
談一談javascript異步
javascript異步與promise
javascript異步之Promise.all()、Promise.race()、Promise.finally()
javascript異步之Promise.resolve()、Promise.reject()
javascript異步之Promise then和catch
javascript異步之async(一)
javascript異步之async(二)
javascript異步實戰
javascript異步總結歸檔

我們之前介紹了javascript異步的相關內容,我們知道javascript以同步,單線程的方式執行主線程代碼,將異步內容放入事件隊列中,當主線程內容執行完畢就會立即循環事件隊列,直到事件隊列爲空,當用產生用戶交互事件(鼠標點擊,點擊鍵盤,滾動屏幕等待),會將事件插入事件隊列中,然後繼續執行。
處理異步邏輯最常用的方式是什麼?沒錯這就是我們今天要說的---回調

js回調函數

如你所知,函數是對象,所以可以存儲在變量中,
所以函數還有以下身份:

  1. 可以作爲函數的參數
  2. 可以在函數中創建
  3. 可以在函數中返回

當一個函數a以一個函數作爲參數或者以一個函數作爲返回值時,那麼函數a就是高階函數
回調函數
百度百科

回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作爲參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對該事件或條件進行響應。

維基百科

在計算機程序設計中,回調函數,或簡稱回調(Callback 即call then back 被主函數調用運算後會返回主函數),是指通過函數參數傳遞到其它代碼的,某一塊可執行代碼的引用。這一設計允許了底層代碼調用在高層定義的子程序。

回調函數,幾乎每天我們都在用

      setTimeout(() => {
        console.log("這是回調函數");
      }, 1000);
      const hero=['郭靖','黃蓉']
      hero.forEach(item=>{
        console.log(item);
      })

回調函數解決了哪些問題

舉一個簡單的:

      let girlName = "裘千尺"

      function hr() {
        girlName = "黃蓉"
        console.log(`我是${girlName}`);
      }

      function gj() {
        console.log(`${girlName}你好,我是郭靖,認識一下吧`);
      }
      hr()
      gj()

輸出,重點看輸出順序

//=>我是黃蓉
//=>黃蓉你好,我是郭靖,認識一下吧

上面的代碼輸出是沒什麼懸念的,不存在異步,都單線程同步執行,最後郭靖和黃蓉相識
如果這時候黃蓉很忙,出現了異步,會怎麼樣?

      let girlName = "裘千尺"

      function hr() {
        setTimeout(() => {
          girlName = "黃蓉"
          console.log('我是黃蓉');
        }, 0);
      }

      function gj() {
        console.log(`${girlName}你好,我是郭靖,認識一下吧`);
      }
      hr()
      gj()

輸出,重點看輸出順序

//=>裘千尺你好,我是郭靖,認識一下吧
//=>我是黃蓉

雖然定時器是0ms,但是也導致了郭靖和黃蓉的擦肩而過,這不是我們期望的結果,hr函數存在異步,只有等主線程的內容走完,才能走異步函數
所以最簡單的辦法就是使用回調函數解決這種問題,gj函數依賴於hr函數的執行結果,所以我們把gj作爲hr的一個回調函數


      let girlName = "裘千尺"

      function hr(callBack) {
        setTimeout(() => {
          girlName = "黃蓉"
          console.log('我是黃蓉');
          callBack()
        }, 0);
      }

      function gj() {
        console.log(`${girlName}你好,我是郭靖,認識一下吧`);
      }
      hr(gj)

輸出,重點看輸出順序

//=>我是黃蓉
//=>黃蓉你好,我是郭靖,認識一下吧

⚠️:當回調函數作爲參數時,不要帶後面的括號!我們只是傳遞函數的名稱,不是傳遞函數的執行結果
上面小栗子貌似的很簡單,我們繼續

嵌套回調和鏈式回調

我們把昨天的demo做一下升級
引入了lodash:處理按鈕點擊防抖
axios,集成了promis,但promise不是我們今天討論的內容,我們只使用axios的ajax請求接口功能
easy-mock:接口數據,用來實現ajax請求(數據是假的,但是請求是真的)

嵌套回調

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>javascript回調</title>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script src="https://cdn.bootcss.com/lodash.js/4.17.11/lodash.min.js"></script>
</head>

<body>
  <button>點擊</button>
  <script>
    {
      const btn = document.querySelector('button')
      btn.onclick = () => {
        _.debounce(() => {
          axios.get('https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock')
            .then(data => {
              console.log("ajax返回成功");
              myData = data.data
              console.log(myData);
            })
            .catch(error => {
              console.log("ajax返回失敗");
            })
        }, 500)()
      }
    }
  </script>
</body>

</html>

仔細看代碼,不難發現,這是一個典型的嵌套回調,我們分析一下
第一層異步,用戶交互,來自按鈕的點擊事件
第二層異步,按鈕去抖,來自lodash下debounce的500ms延時
第三次異步,ajax請求,處理後臺接口數據
拿到數據後我們沒有繼續做處理,在實際工作中可能還存在異步,還會繼續嵌套,會形成一個三角形的縮進區域

再繼續嵌套,就會形成所說的“回調地獄”,就是回調的層級太多了,代碼維護成本會高很多
上面的栗子最多算是入門毀掉地獄,我們看一下這個

      function funA(callBack) {
        console.log("A");
        setTimeout(() => {
          callBack()
        }, 10);
      }

      function funB() {
        console.log("B");
      }

      function funC(callBack) {
        console.log("C");
        setTimeout(() => {
          callBack()
        }, 100);
      }

      function funD() {
        console.log("D");
      }

      function funE() {
        console.log("E");
      }

      function funF() {
        console.log("F");
      }
//從這裏開始執行
      funA(() => {
        funB()
        funC(() => {
          funD()
        })
        funE()
      })
      funF()

(這段代碼,帶回調的都是異步邏輯)你能很快的看出這段代碼的執行順序嗎?
順序如下:A、F、B、C、E、D
一般正常人不會這麼嵌套多層,層級一多,就會考慮拆分

鏈式回調

      const btn = document.querySelector('button')
      //監聽按鈕點擊事件
      btn.onclick = () => {
        debounceFun()
      }
      //去抖動
      const debounceFun = _.debounce(() => {
        ajax()
      }, 500)
      //ajax 請求
      const ajax = function () {
        axios.get('https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock')
          .then(data => {
            console.log("ajax返回成功");
            myData = data.data
            console.log(myData);
          })
          .catch(error => {
            console.log("ajax返回失敗");
          })
      }

我相信很多人都會通過這種鏈式回調的方式處理異步回調,因爲可讀性比嵌套回調要搞,但是維護的成本可能要高很多
上面的栗子,三個異步函數之間只有執行順序上的關聯,並沒有數據上的關聯,但是實際開發中的情況要比這個複雜,

回調函數參數校驗

我們舉一個簡單的栗子

      let girlName = "裘千尺"

      function hr(callBack) {
        setTimeout(() => {
          girlName = "黃蓉"
          console.log('我是黃蓉');
          callBack(girlName)
        }, 0);
      }

      function gj(love) {
        console.log(`${girlName}你好,我是郭靖,認識一下吧,我喜歡${love}`);
      }
      hr(gj)

gj作爲hr的回調函數,並且hr將自己的一個變量傳遞給gj,gj在hr的回調中執行,
仔細看這種寫法並不嚴謹,
如果gj並不只是一個function類型會怎麼樣?
如果love的實參並不存在會怎麼樣?
況且這只是一個簡單的栗子
所以回調函數中,參數的校驗是很有必要的,回調函數鏈拉的越長,校驗的條件就會越多,代碼量就會越多,隨之而來的問題就是可讀性和可維護性就會降低。

還是回調函數的校驗

但我們引用了第三方的插件或庫的時候,有時候難免要出現異步回調的情況,一個栗子:
xx支付,當用戶發起支付後,我們將自己的一個回調函數,傳遞給xx支付,xx支付比較耗時,執行完之後,理論上它會去執行我們傳遞給他的回調函數,是的理論上是這樣的,我們把回調的執行權交給了第三方,隱患隨之而來
第三方支付,多次調用我們的回調函數怎麼辦?
第三方支付,不調用我們的回調函數怎麼辦?
當我們把回調函數的執行權交給別人時,我們也要考慮各種場景可能會發生的問題

總結一下:
回調函數簡單方便,但是坑也不少,用的時候需要多注意校驗

原文鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章