無論你是 JavaScript 的初學者還是專家,無論是為了求職面試還是日常開發工作,我們經常會遇到這樣的情況:給出幾行代碼,我們需要知道它們的輸出內容和順序。由于 JavaScript 是一種單線程語言,我們可以得出以下結論:
JavaScript 按照語句出現的順序執行。
此時,讀者可能會說:我知道 JS 是一行一行執行的,為什么還要特別指出呢?冷靜下來;正因為 JS 是一行一行執行的,我們假設所有的 JS 都是這樣工作的:
let a = '1';console.log(a);let b = '2';console.log(b);
然而,實際上 JS 是這樣的:
setTimeout(function(){ console.log('定時器開始了')});new Promise(function(resolve){ console.log('即將執行for循環'); for(var i = 0; i < 10000; i++){ i == 99 && resolve(); }}).then(function(){ console.log('執行then函數')});console.log('代碼執行結束');
遵循 JavaScript 按語句順序執行的概念,我自信地寫下了輸出:
然而,在 Chrome 中驗證時,結果完全錯誤,瞬間迷惑,難道不是按約定的一行一行執行的嗎?
我們需要徹底理解 JavaScript 的執行機制。
JavaScript 是一種單線程語言。盡管在最新的 HTML5 中引入了 Web Worker,但 JavaScript 的單線程核心沒有改變。因此,JavaScript 中的所有“多線程”都是使用單線程模擬的,所有的多線程都是欺騙性的!
由于 JavaScript 是單線程的,就像只有一個窗口的銀行,客戶需要一個接一個地排隊辦理業務。同樣,JavaScript 任務也需要一個接一個地執行。如果一個任務花費太長時間,那么下一個任務就必須等待。所以問題來了:如果我們想瀏覽新聞,但新聞中的高清圖片加載緩慢,我們的網頁是否必須一直卡住,直到圖片完全顯示?因此,聰明的程序員將任務分為兩類:
當我們打開一個網站時,網頁的渲染過程由一堆同步任務組成,如渲染頁面骨架和頁面元素。那些消耗資源多、耗時長的任務,如加載圖片或音樂文件,則是異步任務。為了簡化理解,我們使用思維導圖來說明這一點:
如果用文字描述思維導圖的內容:
我們不禁要問,如何知道主線程執行棧是否為空?JavaScript 引擎有一個監視過程,持續檢查主線程執行棧是否為空。一旦為空,它就會去事件隊列中檢查是否有等待調用的函數。
經過以上描述,一段代碼可能會更直觀:
let data = [];$.ajax({ url: 'www.javascript.com', data: data, success: () => { console.log('發送成功'); }})console.log('代碼執行結束');
上面是一段簡單的 ajax 請求代碼:
通過以上的文字和代碼,相信你對 JavaScript 的執行順序有了初步的了解。接下來,讓我們研究一個高級話題:setTimeout
。
眾所周知,setTimeout 無需過多介紹。我們對它的第一印象是它可以在延遲之后異步執行。我們經常使用它來實現 3 秒延遲執行:
setTimeout(() => { task();}, 3000)console.log('執行 console');
隨著 setTimeout 的使用逐漸增多,問題也隨之而來。有時,即使在代碼中指定了 3 秒的延遲,函數也會在 5 或 6 秒后執行。這可能是什么原因造成的呢?
我們先看一個例子:
setTimeout(() => { task();}, 3000)console.log('執行 console');
根據我們之前的結論,setTimeout 是異步的,所以同步任務 console.log 應該先執行。因此,我們的結論是:
為了驗證,結果是正確的!然后讓我們對之前的代碼做一些修改:
setTimeout(() => { task();}, 3000)sleep(10000000)
乍一看,這似乎類似,但當我們在 Chrome 中執行這段代碼時,發現 console 的執行時間遠遠超過 3 秒。為什么現在需要這么長時間呢?
此時,我們需要重新定義 setTimeout。讓我們來討論上面代碼的執行過程:
經過上述過程,我們了解到 setTimeout 函數會在指定時間后將任務(在這個例子中是 task())添加到事件隊列中。由于任務在單線程環境中一個接一個地執行,如果前面的任務執行時間過長,執行時間將顯著超過 3 秒。
我們經常遇到類似 setTimeout(fn, 0) 的代碼。0 秒后執行意味著什么?它能立即執行嗎?
答案是否定的。setTimeout(fn, 0) 的意思是指定某個任務在主線程最早的空閑時間執行,不需要等待任何額外的秒數,一旦所有同步任務在棧中完成并且棧變為空。例如:
// 代碼 1console.log('先執行這里');setTimeout(() => { console.log('執行了')}, 0);// 代碼 2console.log('先執行這里');setTimeout(() => { console.log('執行了')}, 3000);
代碼 1 的輸出結果是:
代碼 2 的輸出結果是:
關于 setTimeout 需要注意的是,即使主線程空閑,0 毫秒也無法實現。根據 HTML 標準,最小值為 4 毫秒。感興趣的同學可以自行探索。
談到 setTimeout,我們不能錯過它的雙胞胎兄弟 setInterval。它們很相似,只不過后者是循環執行的。從執行順序來看,setInterval 會在每個指定的間隔時間將注冊的函數放入事件隊列。如果前一個任務花費太長時間,它也需要等待。
唯一需要注意的是,對于 setInterval(fn, ms),我們已經知道 fn 不會每 ms 秒執行一次,而是在每 ms 秒將一個新的 fn 實例放入事件隊列。如果 setInterval 的回調函數(fn)花費的時間超過了延遲時間(ms),那么將不會有明顯的時間間隔。請仔細思考這句話。
我們已經研究了傳統的定時器,接下來,我們將探索 Promise 和 process.nextTick(callback) 的表現。
Promise 的定義和功能在本文中不會詳細展開。而 process.nextTick(callback) 類似于 Node.js 中的 “setTimeout”,在事件循環的下一輪調用回調函數。
切入正題,除了同步任務和異步任務的廣義定義外,我們還有更精細的任務定義:
不同類型的任務將進入相應的事件隊列;例如,setTimeout 和 setInterval 將進入同一個事件隊列。
事件循環中的事件順序決定了 JavaScript 代碼的執行順序。在進入整體代碼(宏任務)后,它開始其第一次循環。然后,它執行所有的微任務。接下來,它再次從宏任務開始,直到一個任務隊列完成,再次執行所有的微任務。聽起來有點復雜;讓我們用本文前面的一個代碼片段來說明:
setTimeout(function() { console.log('setTimeout');})new Promise(function(resolve) { console.log('promise');}).then(function() { console.log('then');})console.log('console');
事件循環、宏任務和微任務之間的關系如圖所示:
我們分析一段更復雜的代碼,看看您是否理解了 JavaScript 的執行機制:
console.log('1');setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') })})process.nextTick(function() { console.log('6');})new Promise(function(resolve) { console.log('7'); resolve();}).then(function() { console.log('8')})setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') })})
事件循環第一輪過程分析如下:
第一輪事件循環正式結束,結果輸出為 1, 7, 6, 8。第二輪事件循環從 setTimeout1 宏任務開始:
整個代碼段經過了三輪事件循環,完整輸出為 1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12。
在 Node 環境中的事件監聽依賴于 libuv,與前端環境不完全相同,輸出順序可能會有差異。
JavaScript 的異步性:從一開始,我們就說過 JavaScript 是單線程語言。無論使用什么新框架或語法糖來實現所謂的異步性,都是通過同步方法模擬的。牢牢把握單線程這一點非常重要。
事件循環:事件循環是 JavaScript 實現異步操作的方法,也是其執行機制。
JavaScript 的執行與運行:執行和運行有很大區別。JavaScript 的執行方式在不同環境中有所不同,如 Node.js、瀏覽器、Ringo 等。然而,運行大多指 JavaScript 解析引擎,保持一致。
setImmediate:有許多類型的微任務和宏任務,如 setImmediate 等,它們的執行有共同點。感興趣的同學可以自行探索。
最后但同樣重要的是:JavaScript 是單線程語言,事件循環是其執行機制。 牢牢掌握這兩個基本點,認真學習 JavaScript,很快實現成為優秀前端開發者的偉大夢想!
本文鏈接:http://www.tebozhan.com/showinfo-26-95556-0.html這次,徹底理解 JavaScript 的執行機制
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 如何更改 .NET 中的默認時區?
下一篇: 接口性能優化的11個小技巧