在 JavaScript 中,我們常常會遇到需要按順序執行一系列非同步任務的情況,比如依次執行三個任務 firstTask、secondTask 和 thirdTask。
如果這些任務依次完成後還有後續操作,我們通常使用回調函數來控制這個順序。
本文將通過兩種不同的調用方式,來解釋如何控制執行順序,並展示每種方式的結果。
案例介紹
假設我們有三個函數:firstTask、secondTask 和 thirdTask。
這三個函數模擬非同步操作,使用 setTimeout 進行 1 秒的延遲。
它們各自完成後會執行一個回調函數:
// 定義第一個任務,接收一個回調函數作為參數
function firstTask(callback) {
setTimeout(() => {
console.log("完成任務1"); // 顯示「完成任務1」
callback(); // 執行回調函數
}, 1000);
}
// 定義第二個任務,接收一個回調函數作為參數
function secondTask(callback) {
setTimeout(() => {
console.log("完成任務2"); // 顯示「完成任務2」
callback(); // 執行回調函數
}, 1000);
}
// 定義第三個任務,接收一個回調函數作為參數
function thirdTask(callback) {
setTimeout(() => {
console.log("完成任務3"); // 顯示「完成任務3」
callback(); // 執行回調函數
}, 1000);
}目標是按順序執行這三個任務,並在所有任務完成後打印「所有任務完成」。
接下來,我們將比較兩種不同的調用方式。
方法一:嵌套回調
首先,我們可以使用嵌套回調來控制順序:
firstTask(() => {
secondTask(() => {
thirdTask(() => {
console.log("所有任務完成");
});
});
});在這種寫法中,secondTask 被放在 firstTask 的回調函數中,而 thirdTask 被放在 secondTask 的回調函數中。
這樣,只有當 firstTask 完成後,才會開始 secondTask,並且只有當 secondTask 完成後,才會開始 thirdTask。
這段代碼的輸出結果是:
完成任務1
完成任務2
完成任務3
所有任務完成這種方式使用嵌套的回調函數來確保執行順序,逐層傳遞回調函數,從而達到 順序控制 的效果。
概念解析:嵌套回調與 Call Stack
當我們調用 firstTask 時,JavaScript 引擎會先執行 firstTask,並設置一個 1 秒延遲的 setTimeout。
在 1 秒之後,firstTask 的回調函數(即 secondTask)會被加入到 事件佇列,並等待主執行緒空閒時依次執行。
secondTask 和 thirdTask 同理,最終達到依次執行的效果。
方法二:直接傳遞嵌套函數 firstTask(secondTask(thirdTask))
另一種常見的嘗試是直接將 thirdTask 作為參數傳給 secondTask,然後再傳遞給 firstTask,即:
firstTask(secondTask(thirdTask));乍看之下,這樣的寫法似乎也可以達到按順序執行的效果,但實際上並不是這樣。為什麼呢?讓我們一步步來分析。
- 首先求值
thirdTask():當 JavaScript 遇到firstTask(secondTask(thirdTask))時,它會遵循「從內到外的求值順序」來處理。也就是說,thirdTask會立即被執行,而不是等到firstTask完成後才執行。這稱為 求值順序(Order of Evaluation)。 - 回傳結果作為參數傳遞:
thirdTask()被執行後,由於沒有返回值,因此它的結果是undefined。這個undefined被作為參數傳遞給secondTask。 - 執行
secondTask(undefined):現在,secondTask立即執行,並接收undefined作為它的參數。執行過程中,secondTask會設置一個setTimeout,但它不會等待thirdTask完成。 - 執行
firstTask(undefined):同樣地,firstTask最後會執行,並設置一個setTimeout,等待 1 秒後輸出「完成任務1」。
這樣的寫法不會按照預期順序執行,而是所有任務的 setTimeout 幾乎同時開始計時。結果的輸出順序是無法保證的,可能是:
完成任務2
完成任務1
完成任務3為什麼會產生這個問題?
關鍵在於 JavaScript 的求值順序與 Call Stack。當 firstTask(secondTask(thirdTask)) 被調用時:
- JavaScript 先執行最內層的
thirdTask()並返回undefined。 - 接著,它將
undefined作為參數傳給secondTask,導致secondTask立即執行並設置計時器。 - 最後,
firstTask也接收undefined作為參數並立即執行,設置計時器。
因為這些 setTimeout 計時器是同時設置的,導致它們的回調無法按順序執行。
求值順序補充:
「求值順序」(Order of Evaluation),有時也稱作 「運算子優先級」(Operator Precedence) 和 「運算子結合性」(Operator Associativity)。
在 JavaScript 中,當一個表達式包含多層嵌套的函數調用時,JavaScript 引擎會遵循一定的順序來對每個子表達式求值,從最內層到最外層。
求值順序與嵌套函數調用
當遇到嵌套函數調用(如 firstTask(secondTask(thirdTask)))時,JavaScript 會先求值最內層的表達式 thirdTask(),將其結果傳給外層的 secondTask,接著再傳遞給最外層的 firstTask。
這種順序遵循 從內到外 的求值原則。
專有名詞
在編程語言的設計中,這種「由內到外」的求值順序主要是根據兩個概念來實現的:
- 「運算子優先級」(Operator Precedence):
- 决定了在一個包含多個運算子的表達式中,哪些運算子先被處理。函數調用的運算子(
())具有較高的優先級,因此thirdTask()會先被執行。
- 决定了在一個包含多個運算子的表達式中,哪些運算子先被處理。函數調用的運算子(
- 「運算子結合性」(Operator Associativity):
- 決定了當運算子的優先級相同時,求值的方向。大多數函數調用都是「由內到外」,即從最內層開始依次求值。
如何應用這些概念
因為 JavaScript 的求值順序是由內到外,當寫 firstTask(secondTask(thirdTask)) 時,最內層的 thirdTask() 會先執行,然後再執行 secondTask 和 firstTask。
這種 先求內層表達式,再將結果向外傳遞 的方式是嵌套函數的一般執行模式。
結論:如何選擇控制順序的方法?
- 嵌套回調:嵌套回調可以確保非同步任務依次執行,適合少量依次執行的操作。但過度嵌套會導致「回調地獄」,代碼難以維護。
- 直接傳遞嵌套函數:直接傳遞嵌套函數無法控制順序,因為最內層函數會先求值並立即執行。這種寫法並不適合需要順序執行的情況。
理解這兩種方法的區別,有助於選擇合適的代碼結構控制任務的執行順序。
希望這篇文章能幫助你在 JavaScript 中順利掌握異步操作的控制!