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誕生於瀏覽器中,最初的作用是響應用戶操作,比如onClick
、onMouseOver
、onChange
、onSubmit
等等。它如何在同步程式模型下實現這一點呢?
答案就在它的環境中。瀏覽器提供了一種處理此類功能的方式,它提供了一組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引入了幾個功能,可以幫助我們處理非同步程式碼,而不需要使用回呼函式:
- Promises(承諾)(ES2015)
- Async/Await(異步/等待)(ES2017)