前言
JS異步執行機制具有非常重要的地位,尤其體現在回調函數和事件等方面。本文將針對JS異步執行機制進行一個簡單的分析。
從一份代碼講起
下面是兩個經典的JS定時執行函數,這兩個函數的區別相信對JS有一定基礎的同學是十分清楚的。timeout僅僅只會執行一次,而interval則會執行多次。
setTimeout(function (args) {
console.log('timeout')
}, 1000);
setInterval(function (args) {
console.log('interval')
}, 1000);
那么再看一份代碼
setTimeout(function (args) {
console.log('timeout');
setTimeout(arguments.callee, 1000);
}, 1000);
setInterval(function (args) {
console.log('interval')
}, 1000);
這兩份代碼是否存在區別呢?在setTimeout中遞歸調用貌似和setInterval一樣,但是實際上由於JS異步執行機制的問題,導致這兩個函數存在着一定的差異。
如何理解JS異步執行機制
JS是單線程程序,從而避免了並發訪問的一系列問題。但也正是由於單線程這樣一個機制,導致JS的異步執行並不能按照傳統的多線程方式進行異步執行,所有的異步時間要插入到同一個隊列中,依次在主線程中執行。
這里有一張圖片,可以比較好的解釋JS的異步執行機制。
在瀏覽器中,一般情況下會存在三個線程,JS執行引擎,HTTP線程,事件觸發線程。但是需要注意的是,所有的JS核心邏輯都需要在JS執行引擎線程中執行。
例如我們可以使用下面這樣一段代碼發送AJAX請求
var xmlReq = createXMLHTTP();//創建一個xmlhttprequest對象
function testAsynRequest() {
var url = "http://127.0.0.1:5000/";
xmlReq.open("post", url, true);
xmlReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlReq.onreadystatechange = function () {
if (xmlReq.readyState == 4) {
if (xmlReq.status == 200) {
var jsonData = eval('(' + xmlReq.responseText + ')');
alert(jsonData.message);
}
else if (xmlReq.status == 404) {
alert("Requested URL is not found.");
} else if (xmlReq.status == 403) {
alert("Access denied.");
} else {
alert("status is " + xmlReq.status);
}
}
};
xmlReq.send(null);
}
testAsynRequest();//1秒后調用回調函數
while (true) {
}
服務端代碼
from flask import Flask
app = Flask(__name__)
@app.route('/', methods=['POST', 'GET'])
def print():
return 'hello world'
if __name__ == '__main__':
app.run()
這段代碼是否會輸出hello world呢?經過測驗,發現並不會輸出HelloWorld,瀏覽器會進入假死狀態。造成這種情況的原因正是JS異步回調單線程的運行機制。在發送HTTP請求以后,HTTP請求會啟動一個線程進行發送,收到響應以后會,事件觸發線程會將響應事件加入到等待隊列中,等待JS引擎空閑后執行。
但是由於while(true)導致JS引擎永遠不存在空閑,從而導致響應事件一致無法觸發。
重新思考
通過一個簡單的AJAX DEMO,可以簡單了解了JS時間執行的一個流程。那么針對上面的那張圖片,和最開始提出的settimeout的問題,JS又是如何調度和處理的呢?
JS在定時器函數初始化以后就會開始執行定時任務,到達時間之后如果此時JS引擎空閑,則會直接執行定時任務,否則會將定時任務加入到等待隊列中。
對於加入到等待隊列中的任務來說,會在JS引擎空閑的時候再不斷進行執行。因此如果此時引擎並非空閑,那么setTimeout會等待一段時間后才能執行。
對於setInterval來說,也是需要加入到等待隊列中的,但是setInterval並不會因為加入到等待隊列中而停止計時,此時如果到了第二個Interval,而第一個Interval還沒有開始執行,那么此時隊列中舊有存在兩個Interval可能,如果這樣累加下去,那么就可能會陷入大量Interval的累加,造成線程嚴重阻塞的問題,因此JS引擎做了一個輕度的優化,如果隊列中有Interval,那么這個Interval不會加入隊列。但是如果Interval已經pop出隊列開始執行,那么Interval將會加入隊列。
針對上面的分析,我們可以得出一個結論,相比於setTimeout函數遞歸調用,在JS中由於單線程的異步執行機制,setInterval執行的頻率會更高。因為setTimeout在執行完成以后才會開始下一輪定時任務,但是setInterval是持續執行定時任務,尤其是在setTimeout里的任務執行時間較長的時候,setInterval和setTimeout會有比較明顯的頻率差異。