JS異步編程之callback

爲何 JS 是單線程?

衆所周知,Javascript 語言的執行環境是"單線程"(single thread)。javascript

所謂"單線程",就是指一次只能完成一件任務。若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。java

而瀏覽器是多線程的,JS 線程就是其中一個:node

  • 瀏覽器 GUI 渲染線程
  • JavaScript 引擎線程
  • 瀏覽器定時觸發器線程
  • 瀏覽器事件觸發線程
  • 瀏覽器 http 異步請求線程

瀏覽器線程知識中重要的一點是:編程

GUI渲染進程和 JavaScript 引擎進程是互斥的,由於若是這兩個線程能夠同時運行的話, JavaScript 的 DOM 操做將會擾亂渲染線程執行渲染先後的數據一致性。並且若是 DOM 一變化,界面就馬上從新渲染,效率必然很低

因此 JS 主線程執行任務時,瀏覽器渲染線程處於掛起狀態。json

同理,若是 JS 採用多線程同步的模型,那麼如何保證同一時間修改了 DOM, 究竟是哪一個線程先生效呢?從操做系統調度多線程的上下文開銷,到實際編程裏的鎖、線程同步等問題,都讓開發變得比較困難。瀏覽器

因此 JS 最終採用了單線程的事件模型。多線程

我以前的文章《JS專題之事件循環》也有講過這塊內容,歡迎翻閱。異步

1、同步與異步

單線程模式這種排隊執行的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。

爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)。async

那同步和異步的區別是什麼?異步編程

咱們想象一個很常見的場景:咱們去麪館吃牛肉麪,櫃檯人不少,前面在排隊下單。

這個時候,同步就是,收銀員收了你的錢,告訴你要在櫃檯站着等面煮好,煮好後,就端面開吃,後面的人也只能等前面的人面煮好了才能付款下單而後等着面煮好端走~

而異步就是,收銀員收了你的錢,而後給了你一張小票,小票上有一個你的編號,收銀員告訴你,能夠去座位上,你的面一煮好,會大聲叫你,你就來端面開吃。

咱們能夠看出,咱們是過程的調用者,麪館是被調用者,牛肉麪煮好,是咱們想要的結果,同步是調用者須要主動地等待這個結果。異步是被動的等待結果,當被調用者有結果了,就會經過消息機制或者回調機制告訴調用者結果。

同步和異步關注的是消息通訊機制,同步就是在發出一個 調用時,在沒有獲得結果以前,該 調用就不返回。可是一旦調用返回,就獲得返回值了。

而異步則是相反,調用在發出以後,這個調用就直接返回了,因此沒有返回結果, 而是在調用發出後,被調用者經過狀態、通知來通知調用者,或經過回調函數處理這個調用。

以上:

  • 下單吃麪是發起調用函數
  • 端面開吃的回調函數
  • 煮好的面是調用的結果,也是回調函數的參數

將例子抽象成僞代碼:

orderNoodle("牛肉麪", function(noodle) {
        // 端面
        getNoodle();
        // 吃麪
        eatNoodle();
});

3、事件循環

關於事件循環如何執行異步代碼能夠翻閱前面的文章《JS專題之事件循環》,這裏大概提一下。

若是遇到異步事件,JS 引擎會把事件函數壓入執行調用棧,但瀏覽器識別到它是異步事件後,會將其彈出執行棧,當異步函數有返回結果後,JS 引擎將異步事件的回調函數放入事件隊列中,若是執行調用棧爲空,就將回調函數壓入執行調用棧執行。

4、回調函數

在 JavaScript 中,函數 function 做爲一等公民,使用上很是自由,不管調用它,或者做爲參數,或者做爲返回值均可以。

由於單線程異步的特色,後來在 JS 中,慢慢將函數的業務重點轉移到了回調函數中。

function step1(cb) {
    console.log("step1");
    cb()
}

function step2(){
    console.log("step2");
}

step1(step2);  // step1  step2

代碼會按前後順序執行 step1, step2。

如今假設咱們有這樣的需求:請求文件1後,獲取文件1 中的數據後請求文件2,獲取文件 2 中的數據後,又請求文件三。

var fs = require("fs");

fs.readFile("./file1.json", function(err, data1) {
    fs.readFile("./file2.json", function (err, data2) {
        fs.readFile("./file3.json", function(err, data3) {
            
        })
    })
})

5、回調函數的問題

由第四節能夠看出,回調函數的寫法存在不少問題。

  1. 回調地獄(洋蔥模型)

當多個異步事務多級依賴時,回調函數會造成多級的嵌套,被花括號一層層包括,代碼變成
金字塔型結構,也被稱爲回調地獄和洋蔥模型。

在回調地獄的狀況下,代碼邏輯的梳理,流程的控制,代碼封裝維護,錯誤處理都變得愈來愈困難。

  1. 異常處理

try...catch 是被設計成捕獲當前執行環境的異常,意思是隻能捕獲同步代碼裏面的異常,異步調用裏面的異常沒法捕獲。

function readFile(fileName) {
    setTimeout(function () {
      throw new Error("類型錯誤");
    }, 1000);
}
try {
    readFile('./file1.json');
} catch (e) {
    // 若是異步事件出錯,打印不出來錯誤信息
    console.log('err', e);
}

在 nodejs 對回調函數採用 error first 的思想,回調函數的第一個參數保留給一個錯誤error對象,若是有錯誤發生,錯誤將經過第一個參數err返回。

緣由是一個有回調函數的函數,執行分兩段,第一段執行完以後,任務所在的上下文環境就已經結束了。在這之後拋出的錯誤,原來的上下文已經沒法捕捉,只能當作參數,傳入第二階段。

fs.readFile('/etc/passwd', 'utf8', function (err, data) {
    if(err) {
        console.log(err)
        return;
    }
});

總結

回調函數是 JS 異步編程中的基石,但同時也存在不少問題,不太適合人類天然語言的線性思惟習慣。

接下來幾篇文章,我將梳理 JS 中異步編程中的歷史演進中 Promise, generator, async&await 相關的內容,歡迎關注。

圖片描述

相關文章
相關標籤/搜索