Javascript 簡(jiǎn)介:回調(diào)

2023-02-17 10:53 更新

我們?cè)谶@里的示例中使用了瀏覽器方法

為了演示回調(diào)、promise 和其他抽象概念的使用,我們將使用一些瀏覽器方法:具體地說(shuō),是加載腳本和執(zhí)行簡(jiǎn)單的文檔操作的方法。

如果你不熟悉這些方法,并且對(duì)它們?cè)谶@些示例中的用法感到疑惑,那么你可能需要閱讀本教程 下一部分 中的幾章。

但是,我們會(huì)盡全力使講解變得更加清晰。在這兒不會(huì)有瀏覽器方面的真正復(fù)雜的東西。

JavaScript 主機(jī)(host)環(huán)境提供了許多函數(shù),這些函數(shù)允許我們計(jì)劃 異步 行為(action)。換句話(huà)說(shuō),我們現(xiàn)在開(kāi)始執(zhí)行的行為,但它們會(huì)在稍后完成。

例如,setTimeout 函數(shù)就是一個(gè)這樣的函數(shù)。

這兒有一些實(shí)際中的異步行為的示例,例如加載腳本和模塊(我們將在后面的章節(jié)中介紹)。

讓我們看一下函數(shù) loadScript(src),該函數(shù)使用給定的 src 加載腳本:

function loadScript(src) {
  // 創(chuàng)建一個(gè) <script> 標(biāo)簽,并將其附加到頁(yè)面
  // 這將使得具有給定 src 的腳本開(kāi)始加載,并在加載完成后運(yùn)行
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

它將一個(gè)新的、帶有給定 src 的、動(dòng)態(tài)創(chuàng)建的標(biāo)簽 <script src="…"> 插入到文檔中。瀏覽器將自動(dòng)開(kāi)始加載它,并在加載完成后執(zhí)行它。

我們可以像這樣使用這個(gè)函數(shù):

// 在給定路徑下加載并執(zhí)行腳本
loadScript('/my/script.js');

腳本是“異步”調(diào)用的,因?yàn)樗鼜默F(xiàn)在開(kāi)始加載,但是在這個(gè)加載函數(shù)執(zhí)行完成后才運(yùn)行。

如果在 loadScript(…) 下面有任何其他代碼,它們不會(huì)等到腳本加載完成才執(zhí)行。

loadScript('/my/script.js');
// loadScript 下面的代碼
// 不會(huì)等到腳本加載完成才執(zhí)行
// ...

假設(shè)我們需要在新腳本加載后立即使用它。它聲明了新函數(shù),我們想運(yùn)行它們。

但如果我們?cè)?nbsp;loadScript(…) 調(diào)用后立即執(zhí)行此操作,這將不會(huì)有效。

loadScript('/my/script.js'); // 這個(gè)腳本有 "function newFunction() {…}"

newFunction(); // 沒(méi)有這個(gè)函數(shù)!

自然情況下,瀏覽器可能沒(méi)有時(shí)間加載腳本。到目前為止,loadScript 函數(shù)并沒(méi)有提供跟蹤加載完成的方法。腳本加載并最終運(yùn)行,僅此而已。但我們希望了解腳本何時(shí)加載完成,以使用其中的新函數(shù)和變量。

讓我們添加一個(gè) callback 函數(shù)作為 loadScript 的第二個(gè)參數(shù),該函數(shù)應(yīng)在腳本加載完成時(shí)執(zhí)行:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

onload 事件在 資源加載:onload,onerror 一文中有描述,它通常會(huì)在腳本加載和執(zhí)行完成后執(zhí)行一個(gè)函數(shù)。

現(xiàn)在,如果我們想調(diào)用該腳本中的新函數(shù),我們應(yīng)該將其寫(xiě)在回調(diào)函數(shù)中:

loadScript('/my/script.js', function() {
  // 在腳本加載完成后,回調(diào)函數(shù)才會(huì)執(zhí)行
  newFunction(); // 現(xiàn)在它工作了
  ...
});

這是我們的想法:第二個(gè)參數(shù)是一個(gè)函數(shù)(通常是匿名函數(shù)),該函數(shù)會(huì)在行為(action)完成時(shí)運(yùn)行。

這是一個(gè)帶有真實(shí)腳本的可運(yùn)行的示例:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(script);
  document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  alert(`酷,腳本 ${script.src} 加載完成`);
  alert( _ ); // _ 是所加載的腳本中聲明的一個(gè)函數(shù)
});

這被稱(chēng)為“基于回調(diào)”的異步編程風(fēng)格。異步執(zhí)行某項(xiàng)功能的函數(shù)應(yīng)該提供一個(gè) callback 參數(shù)用于在相應(yīng)事件完成時(shí)調(diào)用。(譯注:上面這個(gè)例子中的相應(yīng)事件是指腳本加載)

這里我們?cè)?nbsp;loadScript 中就是這么做的,但當(dāng)然這是一種通用方法。

在回調(diào)中回調(diào)

我們?nèi)绾我来渭虞d兩個(gè)腳本:第一個(gè),然后是第二個(gè)?

自然的解決方案是將第二個(gè) loadScript 調(diào)用放入回調(diào)中,如下所示:

loadScript('/my/script.js', function(script) {

  alert(`酷,腳本 ${script.src} 加載完成,讓我們繼續(xù)加載另一個(gè)吧`);

  loadScript('/my/script2.js', function(script) {
    alert(`酷,第二個(gè)腳本加載完成`);
  });

});

在外部 loadScript 執(zhí)行完成時(shí),回調(diào)就會(huì)發(fā)起內(nèi)部的 loadScript。

如果我們還想要一個(gè)腳本呢?

loadScript('/my/script.js', function(script) {

  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
      // ...加載完所有腳本后繼續(xù)
    });

  });

});

因此,每一個(gè)新行為(action)都在回調(diào)內(nèi)部。這對(duì)于幾個(gè)行為來(lái)說(shuō)還好,但對(duì)于許多行為來(lái)說(shuō)就不好了,所以我們很快就會(huì)看到其他變體。

處理 Error

在上述示例中,我們并沒(méi)有考慮出現(xiàn) error 的情況。如果腳本加載失敗怎么辦?我們的回調(diào)應(yīng)該能夠?qū)Υ俗鞒龇磻?yīng)。

這是 loadScript 的改進(jìn)版本,可以跟蹤加載錯(cuò)誤:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

加載成功時(shí),它會(huì)調(diào)用 callback(null, script),否則調(diào)用 callback(error)。

用法:

loadScript('/my/script.js', function(error, script) {
  if (error) {
    // 處理 error
  } else {
    // 腳本加載成功
  }
});

再次強(qiáng)調(diào),我們?cè)?nbsp;loadScript 中所使用的方案其實(shí)很普遍。它被稱(chēng)為“Error 優(yōu)先回調(diào)(error-first callback)”風(fēng)格。

約定是:

  1. ?callback? 的第一個(gè)參數(shù)是為 error 而保留的。一旦出現(xiàn) error,?callback(err)? 就會(huì)被調(diào)用。
  2. 第二個(gè)參數(shù)(和下一個(gè)參數(shù),如果需要的話(huà))用于成功的結(jié)果。此時(shí) ?callback(null, result1, result2…)? 就會(huì)被調(diào)用。

因此,單一的 callback 函數(shù)可以同時(shí)具有報(bào)告 error 和傳遞返回結(jié)果的作用。

厄運(yùn)金字塔

乍一看,它像是一種可行的異步編程方式。的確如此,對(duì)于一個(gè)或兩個(gè)嵌套的調(diào)用看起來(lái)還不錯(cuò)。

但對(duì)于一個(gè)接一個(gè)的多個(gè)異步行為,代碼將會(huì)變成這樣:

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...加載完所有腳本后繼續(xù) (*)
          }
        });

      }
    });
  }
});

在上面這段代碼中:

  1. 我們加載 ?1.js?,如果沒(méi)有發(fā)生錯(cuò)誤。
  2. 我們加載 ?2.js?,如果沒(méi)有發(fā)生錯(cuò)誤……
  3. 我們加載 ?3.js?,如果沒(méi)有發(fā)生錯(cuò)誤 —— 做其他操作 ?(*)?。

隨著調(diào)用嵌套的增加,代碼層次變得更深,維護(hù)難度也隨之增加,尤其是我們使用的是可能包含了很多循環(huán)和條件語(yǔ)句的真實(shí)代碼,而不是例子中的 ...。

有時(shí)這些被稱(chēng)為“回調(diào)地獄”或“厄運(yùn)金字塔”。


嵌套調(diào)用的“金字塔”隨著每個(gè)異步行為會(huì)向右增長(zhǎng)。很快它就失控了。

所以這種編碼方式不是很好。

我們可以通過(guò)使每個(gè)行為都成為一個(gè)獨(dú)立的函數(shù)來(lái)嘗試減輕這種問(wèn)題,如下所示:

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...加載完所有腳本后繼續(xù) (*)
  }
}

看到了嗎?它的作用相同,但是沒(méi)有深層的嵌套了,因?yàn)槲覀儗⒚總€(gè)行為都編寫(xiě)成了一個(gè)獨(dú)立的頂層函數(shù)。

它可以工作,但是代碼看起來(lái)就像是一個(gè)被撕裂的表格。你可能已經(jīng)注意到了,它的可讀性很差,在閱讀時(shí)你需要在各個(gè)代碼塊之間跳轉(zhuǎn)。這很不方便,特別是如果讀者對(duì)代碼不熟悉,他們甚至不知道應(yīng)該跳轉(zhuǎn)到什么地方。

此外,名為 ?step*? 的函數(shù)都是一次性使用的,創(chuàng)建它們就是為了避免“厄運(yùn)金字塔”。沒(méi)有人會(huì)在行為鏈之外重用它們。因此,這里的命名空間有點(diǎn)混亂。

我們希望還有更好的方法。

幸運(yùn)的是,有其他方法可以避免此類(lèi)金字塔。最好的方法之一就是 “promise”,我們將在下一章中介紹它。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)