JavaScript非同步程式設計與回呼函式(Callbacks)
JavaScript默認是同步的,並且是單線程的。這意味著程式碼無法創建新的線程並且並行運行。了解一下非同步程式碼的含義以及它是什麼樣子。
程式語言中的非同步性
電腦的設計使其成為非同步的。
非同步意味著事情可以獨立於主程式流程發生。
在現代消費者電腦中,每個程式運行一段特定的時間片段,然後停止執行,以讓其他程式繼續執行。這個循環運行得非常快,我們認為我們的電腦同時運行許多程式,但這只是一種幻覺(除了在多處理器計算機上)。
程式內部使用中斷,這是發送給處理器以引起系統注意的信號。
我不打算深入探討這方面的內部細節,只是讓你記住,程式是非同步的很正常,它們在等待時會暫停執行,並且電腦可以在此期間執行其他任務。當程式在等待網絡響應時,它無法阻止處理器停止執行。
通常,程式語言都是同步的,一些語言提供了一種處理非同步的方式,可以在語言本身或通過函式庫中進行處理。C、Java、C#、PHP、Go、Ruby、Swift、Python等都是默認同步的。有些語言通過使用線程來處理非同步,而線程則產生新的進程。
JavaScript
JavaScript默認是同步的,並且是單線程的。這意味著程式碼無法創建新的線程並且並行運行。
程式碼按照順序一行行執行,例如:
1 | const a = 1 |
然而,JavaScript誕生於瀏覽器中,最初的作用是響應用戶操作,比如onClick
、onMouseOver
、onChange
、onSubmit
等等。它如何在同步程式模型下實現這一點呢?
答案就在它的環境中。瀏覽器提供了一種處理此類功能的方式,它提供了一組API。
最近,Node.js引入了一個非阻塞的I/O環境,用於擴展此概念到文件訪問、網絡請求等等。
回呼函式
你無法知道用戶何時會點擊一個按鈕,所以你要做的是,為點擊事件定義一個事件處理函式。這個事件處理函式接受一個函式作為參數,在事件觸發時將調用該函式。
1 | document.getElementById('button').addEventListener('click', () => { |
這就是所謂的回呼函式。
回呼函式就是一個簡單的函式,它作為值傳遞給另一個函式,並且只有在事件發生時才會被執行。我們之所以能夠做到這一點,是因為JavaScript提供了一級函式的功能,這些函式可以被賦值給變量並且可以傳遞給其他函式(稱為高階函式)。
通常會將所有的用戶端代碼包裝在window
對象上的load
事件監聽器中,只有在頁面準備好時才會運行回呼函式:
1 | window.addEventListener('load', () => { |
回呼函式無處不在,不僅僅在DOM事件中使用。
一個常見的例子是定時器:
1 | setTimeout(() => { |
XHR請求也接受回呼函式,這個例子中通過將一個函式賦值給一個屬性,在特定事件發生時調用該函式(在此例中,是請求狀態發生改變):
1 | const xhr = new XMLHttpRequest() |
處理回呼函式中的錯誤
在回呼函式中如何處理錯誤?一種非常常見的策略是使用Node.js採用的策略:在任何回呼函式中,第一個參數是錯誤對象:錯誤優先的回呼函式。
如果沒有錯誤,該對象為null
。如果有錯誤,它包含錯誤的描述和其他信息。
1 | fs.readFile('/file.json', (err, data) => { |
回呼函式的問題
回呼函式對於簡單情況非常好用!
但是,每個回呼函式都會增加一層嵌套,當有很多回呼函式時,程式碼會很快變得很複雜:
1 | window.addEventListener('load', () => { |
這只是一個簡單的4層代碼,但是我見過更多層次的嵌套,這是非常不好的。
那麼,我們該如何解決這個問題呢?
回呼函式的替代方案
從ES6開始,JavaScript引入了幾個功能,可以幫助我們處理非同步程式碼,而不需要使用回呼函式:
- Promises(承諾)(ES2015)
- Async/Await(異步/等待)(ES2017)
tags: [“async programming”, “callbacks”, “asynchronous code”, “JavaScript”, “Programming Languages”]