首頁  >  文章  >  web前端  >  詳解JavaScript事件循環機制

詳解JavaScript事件循環機制

青灯夜游
青灯夜游轉載
2018-10-08 15:30:432299瀏覽

這篇文章跟大家分享了關於JavaScript事件循環機制的相關知識點內容,有興趣的朋友們可以學習參考下。

眾所周知,JavaScript 是一門單執行緒語言,雖然在 html5 中提出了 Web-Worker ,但這並未改變 JavaScript 是單執行緒這一核心。可看HTML規格中的這段話:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

為了協調事件、使用者互動、腳本、UI 渲染和網路處理等行為,使用者引擎必須使用event loops。 Event Loop 包含兩類:一類是基於 Browsing Context ,另一種是基於 Worker ,二者是獨立運作的。以下本文用一個例子,著重講解下基於 Browsing Context 的事件循環機制。

來看下面這段JavaScript 程式碼:

console.log('script start');

setTimeout(function() {
 console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
 console.log('promise1');
}).then(function() {
 console.log('promise2');
});

console.log('script end');

先猜測這段程式碼的輸出順序是什麼,再去瀏覽器控制台輸入一下,看看實際輸出的順序和你猜測出的順序是否一致,如果一致,那就說明,你對JavaScript 的事件循環機制還是有一定了解的,繼續往下看可以鞏固下你的知識;而如果實際輸出的順序和你的猜測不一致,那麼本文下面的部分會為你答疑解惑。

任務佇列

所有的任務可以分為同步任務和非同步任務,同步任務,顧名思義,就是立即執行的任務,同步任務一般會直接進入到主執行緒中執行;而非同步任務,就是非同步執行的任務,例如ajax網路請求,setTimeout 定時函數等都屬於非同步任務,非同步任務會透過任務佇列( Event Queue )的機制來進行協調。具體的可以用下面的圖來大致說明一下:

同步和非同步任務分別進入不同的執行環境,同步的進入主線程,即主執行棧,異步的進入Event Queue 。主執行緒內的任務執行完畢為空,會去 Event Queue 讀取對應的任務,推入主執行緒執行。上述過程的不斷重複就是我們說的 Event Loop (事件循環)。

在事件循環中,每進行一次循環操作稱為tick,透過閱讀規範可知,每一次tick 的任務處理模型是比較複雜的,其關鍵的步驟可以總結如下:

  • 在此次tick 中選擇最先進入佇列的任務( oldest task ),如果有則執行(一次)

  • 檢查是否存在Microtasks ,如果存在則不停地執行,直到清空Microtask Queue

  • #更新render

主執行緒重複執行上述步驟

可以用一張圖來說明下流程:

這裡相信有人會想問,什麼是microtasks ?規範中規定,task分為兩大類, 分別是Macro Task (宏任務)和Micro Task(微任務), 並且每個宏任務結束後, 都要清空所有的微任務,這裡的Macro Task也是我們常說的task ,有些文章並沒有對其做區分,後面文章所提及的task皆看做宏任務( macro task)。

(macro)task 主要包含:script( 整體程式碼)、setTimeout、setInterval、I/O、UI 互動事件、setImmediate(Node.js 環境)

microtask主要包含:Promise、 MutaionObserver、process.nextTick(Node.js 環境)

setTimeout/Promise 等API便是任務來源,而進入任務佇列的則是由他們指定的特定執行任務。來自不同任務來源的任務會進入到不同的任務佇列。其中 setTimeout 與 setInterval 是同源的。

分析範例程式碼

千言萬語,不如就著例子講來的清楚。下面我們可以依照規範,一步一步執行解析下上面的例子,先貼一下例子程式碼(免得你往上翻)。

console.log('script start');

setTimeout(function() {
 console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
 console.log('promise1');
}).then(function() {
 console.log('promise2');
});

console.log('script end');

整體script 作為第一個巨集任務進入主線程,遇到console.log,輸出script start

  • 遇到setTimeout,其回呼函數被分發到巨集任務Event Queue 中

  • 遇到Promise,其then函數被分到到微任務Event Queue 中,記為then1,之後又遇到了then 函數,將其分到微任務Event Queue 中,記為then2

  • 遇到console.log,輸出script end

至此,Event Queue 中存在三個任務,如下表:

宏任務 微任務
########### setTimeout######then1############-#######then2############
  • 执行微任务,首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有微任务

  • 执行 setTimeout 任务,输出 setTimeout 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout

so,你猜对了吗?

看看你掌握了没

再来一个题目,来做个练习:

console.log('script start');

setTimeout(function() {
 console.log('timeout1');
}, 10);

new Promise(resolve => {
 console.log('promise1');
 resolve();
 setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
 console.log('then1')
})

console.log('script end');

这个题目就稍微有点复杂了,我们再分析下:

首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到 promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1; 接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2 至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

用流程图看更清晰:

总结

有个小 tip:从规范来看,microtask 优先于 task 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。

最后的最后,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

以上就是本章的全部内容,更多相关教程请访问JavaScript视频教程

以上是詳解JavaScript事件循環機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:jb51.net。如有侵權,請聯絡admin@php.cn刪除