JavaScript默認是同步的,並且是單線程的。這意味著程式碼無法創建新的線程並且並行運行。了解一下非同步程式碼的含義以及它是什麼樣子。

程式語言中的非同步性

電腦的設計使其成為非同步的。

非同步意味著事情可以獨立於主程式流程發生。

在現代消費者電腦中,每個程式運行一段特定的時間片段,然後停止執行,以讓其他程式繼續執行。這個循環運行得非常快,我們認為我們的電腦同時運行許多程式,但這只是一種幻覺(除了在多處理器計算機上)。

程式內部使用中斷,這是發送給處理器以引起系統注意的信號。

我不打算深入探討這方面的內部細節,只是讓你記住,程式是非同步的很正常,它們在等待時會暫停執行,並且電腦可以在此期間執行其他任務。當程式在等待網絡響應時,它無法阻止處理器停止執行。

通常,程式語言都是同步的,一些語言提供了一種處理非同步的方式,可以在語言本身或通過函式庫中進行處理。C、Java、C#、PHP、Go、Ruby、Swift、Python等都是默認同步的。有些語言通過使用線程來處理非同步,而線程則產生新的進程。

JavaScript

JavaScript默認是同步的,並且是單線程的。這意味著程式碼無法創建新的線程並且並行運行。

程式碼按照順序一行行執行,例如:

const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()

然而,JavaScript誕生於瀏覽器中,最初的作用是響應用戶操作,比如onClickonMouseOveronChangeonSubmit等等。它如何在同步程式模型下實現這一點呢?

答案就在它的環境中。瀏覽器提供了一種處理此類功能的方式,它提供了一組API。

最近,Node.js引入了一個非阻塞的I/O環境,用於擴展此概念到文件訪問、網絡請求等等。

回呼函式

你無法知道用戶何時會點擊一個按鈕,所以你要做的是,為點擊事件定義一個事件處理函式。這個事件處理函式接受一個函式作為參數,在事件觸發時將調用該函式。

document.getElementById('button').addEventListener('click', () => {
    //當按鈕被點擊時執行
})

這就是所謂的回呼函式

回呼函式就是一個簡單的函式,它作為值傳遞給另一個函式,並且只有在事件發生時才會被執行。我們之所以能夠做到這一點,是因為JavaScript提供了一級函式的功能,這些函式可以被賦值給變量並且可以傳遞給其他函式(稱為高階函式)。

通常會將所有的用戶端代碼包裝在window對象上的load事件監聽器中,只有在頁面準備好時才會運行回呼函式:

window.addEventListener('load', () => {
    //視窗已加載
    //執行你想做的事情
})

回呼函式無處不在,不僅僅在DOM事件中使用。

一個常見的例子是定時器:

setTimeout(() => {
    //2秒後執行
}, 2000)

XHR請求也接受回呼函式,這個例子中通過將一個函式賦值給一個屬性,在特定事件發生時調用該函式(在此例中,是請求狀態發生改變):

const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
        xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
    }
}
xhr.open('GET', 'https://yoursite.com')
xhr.send()

處理回呼函式中的錯誤

在回呼函式中如何處理錯誤?一種非常常見的策略是使用Node.js採用的策略:在任何回呼函式中,第一個參數是錯誤對象:錯誤優先的回呼函式

如果沒有錯誤,該對象為null。如果有錯誤,它包含錯誤的描述和其他信息。

fs.readFile('/file.json', (err, data) => {
    if (err !== null) {
        //處理錯誤
        console.log(err)
        return
    }

    //沒有錯誤,處理數據
    console.log(data)
})

回呼函式的問題

回呼函式對於簡單情況非常好用!

但是,每個回呼函式都會增加一層嵌套,當有很多回呼函式時,程式碼會很快變得很複雜:

window.addEventListener('load', () => {
    document.getElementById('button').addEventListener('click', () => {
        setTimeout(() => {
            items.forEach(item => {
                //這裡放你的程式碼
            })
        }, 2000)
    })
})

這只是一個簡單的4層代碼,但是我見過更多層次的嵌套,這是非常不好的。

那麼,我們該如何解決這個問題呢?

回呼函式的替代方案

從ES6開始,JavaScript引入了幾個功能,可以幫助我們處理非同步程式碼,而不需要使用回呼函式: