JavaScript 關(guān)于事件循環(huán)機制的刨析
前言:
這次主要整理一下自己對 Js事件循環(huán)機制,同步,異步任務(wù),宏任務(wù),微任務(wù)的理解,大概率暫時還有些偏差或者錯誤。如果有,十分歡迎各位糾正我的錯誤!
一、事件循環(huán)和任務(wù)隊列產(chǎn)生的原因:
首先,JS是單線程,這樣設(shè)計也是具有合理性的,試想如果一邊進行dom的刪除,另一邊又進行dom的添加,瀏覽器該如何處理?
引用:
“單線程即任務(wù)是串行的,后一個任務(wù)需要等待前一個任務(wù)的執(zhí)行,這就可能出現(xiàn)長時間的等待。但由于類似ajax網(wǎng)絡(luò)請求、setTimeout時間延遲、DOM事件的用戶交互等,這些任務(wù)并不消耗 CPU,是一種空等,資源浪費,因此出現(xiàn)了異步。通過將任務(wù)交給相應(yīng)的異步模塊去處理,主線程的效率大大提升,可以并行的去處理其他的操作。當異步處理完成,主線程空閑時,主線程讀取相應(yīng)的callback,進行后續(xù)的操作,最大程度的利用CPU。此時出現(xiàn)了同步執(zhí)行和異步執(zhí)行的概念,同步執(zhí)行是主線程按照順序,串行執(zhí)行任務(wù);異步執(zhí)行就是cpu跳過等待,先處理后續(xù)的任務(wù)(CPU與網(wǎng)絡(luò)模塊、timer等并行進行任務(wù))。由此產(chǎn)生了任務(wù)隊列與事件循環(huán),來協(xié)調(diào)主線程與異步模塊之間的工作。“”
二、事件循環(huán)機制:
圖解:
首先把JS執(zhí)行代碼操作 分為主線程
,任務(wù)隊列
,任何一段js代碼的執(zhí)行都可以分為以下幾個步驟:
步驟一: 主線程讀取JS代碼,此時為同步環(huán)境,形成相應(yīng)的堆和執(zhí)行棧;
步驟二: 當主線程遇到異步操作的時候,將異步操作交給對應(yīng)的API進行處理;
步驟三: 當異步操作處理完成,推入任務(wù)隊列中
步驟四: 主線程執(zhí)行完畢后,查詢?nèi)蝿?wù)隊列,取出一個任務(wù),并推入主線程進行處理
步驟五: 重復(fù)步驟二、三、四
其中常見的異步操作有:ajax請求,setTimeout,還有類似onclik事件等
等
三、任務(wù)隊列:
同步和異步任務(wù)分別進入不同的執(zhí)行環(huán)境,同步的進入主線程,即主執(zhí)行棧,異步的進入任務(wù)隊列
首先,顧名思義,既然是一個隊列,那么就遵循FIFO
原則
如上示意圖,任務(wù)隊列存在多個,它們的執(zhí)行順序:
同一任務(wù)隊列內(nèi),按隊列順序被主線程取走;
不同任務(wù)隊列之間,存在著優(yōu)先級,優(yōu)先級高的優(yōu)先獲?。ㄈ缬脩鬒/O)
3.1 任務(wù)隊列的類型:
任務(wù)隊列分為 宏任務(wù)(macrotask queue)
和 微任務(wù)(microtask queue)
宏任務(wù)主要包含:script( 整體代碼)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 環(huán)境)
微任務(wù)主要包含:Promise、MutaionObserver、process.nextTick(Node.js 環(huán)境)
3.2 兩者區(qū)別:
微任務(wù)microtask queue:
(1) 唯一,整個事件循環(huán)當中,僅存在一個;
(2) 執(zhí)行為同步,同一個事件循環(huán)中的microtask會按隊列順序,串行執(zhí)行完畢;
PS:所以利用microtask queue可以形成一個同步執(zhí)行的環(huán)境
宏任務(wù)macrotask queue
:
(1) 不唯一,存在一定的優(yōu)先級(用戶I/O部分優(yōu)先級更高)
(2) 異步執(zhí)行,同一事件循環(huán)中,只執(zhí)行一個
3.3 更細致的事件循環(huán)過程
- 一、二、三、步同上
- 主線程查詢?nèi)蝿?wù)隊列,執(zhí)行microtask queue,將其按序執(zhí)行,全部執(zhí)行完畢;
- 主線程查詢?nèi)蝿?wù)隊列,執(zhí)行macrotask queue,取隊首任務(wù)執(zhí)行,執(zhí)行完畢;
- 重復(fù)四、五步驟;
先用一個簡單的例子加深一下理解:
console.log('1, time = ' + new Date().toString()) // 1.進入主線程,執(zhí)行同步任務(wù),輸出1 setTimeout(macroCallback, 0)// 2. 加入宏任務(wù)隊列 // 7.開始執(zhí)行此定時器宏任務(wù),調(diào)用macroCallback,輸出4 new Promise(function (resolve, reject) {//3.加入微任務(wù)隊列 console.log('2, time = ' + new Date().toString())//4.執(zhí)行此微任務(wù)中的同步代碼,輸出2 resolve() console.log('3, time = ' + new Date().toString())//5.輸出3 }).then(microCallback)// 6.執(zhí)行then微任務(wù),調(diào)用microCallback,輸出5 //函數(shù)定義 function macroCallback() { console.log('4, time = ' + new Date().toString()) } function microCallback() { console.log('5, time = ' + new Date().toString()) }
運行結(jié)果:
四、強大的異步專家 process.nextTick()
第一次看見這東西,有點眼熟啊,想了一下好像之前vue項目中 用過 this.$nextTick(callback)
當時說的是 當頁面上元素被重新渲染之后 才會執(zhí)行回調(diào)函數(shù)中的代碼
,不是很理解,暫時記住吧
4.1 process.nextTick()在何時調(diào)用?
任何時候在給定的階段中調(diào)用 process.nextTick(),所有傳遞到 process.nextTick() 的回調(diào)將在事件循環(huán)繼續(xù)之前解析
在事件循環(huán)中,每進行一次循環(huán)操作稱為tick
,知道了這個之后,對理解這個方法什么時候調(diào)用瞬間明白了一些!
再借用別人的例子,加深一下對事件循環(huán)的理解吧:
var flag = false // 1. 變量聲明 Promise.resolve().then(() => { // 2. 將 then 任務(wù)分發(fā)到本輪循環(huán)微任務(wù)隊列中去 console.log('then1') // 8. 執(zhí)行 then 微任務(wù), 打印 then1,flag 此時是 true 了 flag = true }) new Promise(resolve => { // 3. 執(zhí)行 Promise 里 同步代碼 console.log('promise') resolve() setTimeout(() => { // 4. 將定時器里的任務(wù)放到宏任務(wù)隊列中 console.log('timeout2') // 11. 執(zhí)行定時器宏任務(wù) 這邊指定了 10 的等待時長, 因此在另一個定時器任務(wù)之后執(zhí)行了 }, 10) }).then(function () { // 5. 將 then 任務(wù)分發(fā)到本輪循環(huán)微任務(wù)隊列中去 console.log('then2') // 9. 執(zhí)行 then 微任務(wù), 打印 then2,至此本輪 tick 結(jié)束 }) function f1(f) { // 1. 函數(shù)聲明 f() } function f2(f) { // 1. 函數(shù)聲明 setTimeout(f) // 7. 把`setTimeout`中的`f`放到宏任務(wù)隊列中,等本輪`tick`執(zhí)行完,下一次事件循環(huán)再執(zhí)行 } f1(() => console.log('f為:', flag ? '異步' : '同步')) // 6. 打印 `f為:同步` f2(() => { console.log('timeout1,', 'f為:', flag ? '異步' : '同步') // 10. 執(zhí)行定時器宏任務(wù) }) console.log('本輪宏任務(wù)執(zhí)行完') // 7. 打印
運行結(jié)果:
process.nextTick 中的回調(diào)是在當前tick執(zhí)行完之后,下一個宏任務(wù)執(zhí)行之前調(diào)用的。
官方的例子:
let bar; // 這個方法用的是一個異步簽名,但其實它是同步方式調(diào)用回調(diào)的 function someAsyncApiCall(callback) { callback(); } // 回調(diào)函數(shù)在`someAsyncApiCall`完成之前被調(diào)用 someAsyncApiCall(() => { // 由于`someAsyncApiCall`已經(jīng)完成,bar沒有被分配任何值 console.log('bar', bar); // undefined }); bar = 1;
使用 process.nextTick
:
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1;
再看一個含有 process.nextTick
的例子:
console.log('1'); // 1.壓入主線程執(zhí)行棧,輸出1 setTimeout(function () { //2.它的回調(diào)函數(shù)被加入 宏任務(wù)隊列中 //7.目前微任務(wù)隊列為空,所以取出 宏任務(wù)隊列首項,執(zhí)行此任務(wù) console.log('2'); // 輸出2 process.nextTick(function () { // 16.上一次循環(huán)結(jié)束,在下一次宏任務(wù)開始之前調(diào)用,輸出3 console.log('3'); }) new Promise(function (resolve) { //8.執(zhí)行 此promise的同步任務(wù),輸出4,狀態(tài)變?yōu)閞esolve console.log('4'); resolve(); }).then(function () {//9.檢測到異步方法then,將其回調(diào)函數(shù)加入 微任務(wù)隊列中 console.log('5'); // 10. 取出微任務(wù)隊列首項,也就是這個then的回調(diào),執(zhí)行,輸出5 }) }) process.nextTick(function () { // 11.一次事件循環(huán)結(jié)束,執(zhí)行nextTick()的回調(diào),輸出6 console.log('6'); }) new Promise(function (resolve) { //3.執(zhí)行promise中的同步任務(wù) 輸出7,狀態(tài)變?yōu)閞esolve console.log('7'); resolve(); }).then(function () { //4.檢測到異步方法then,將其回調(diào)函數(shù)加入 微任務(wù)隊列中 console.log('8'); //6. 主線程執(zhí)行完畢,取出微任務(wù)隊列中首項,將其回調(diào)函數(shù)壓入執(zhí)行棧,輸出8 }) setTimeout(function () { //5.它的回調(diào)函數(shù) 加入 宏任務(wù)隊列中 //12.此刻,微任務(wù)隊列為空,開始執(zhí)行此宏任務(wù) console.log('9'); // 輸出9 process.nextTick(function () { // 17.此刻 微任務(wù)和宏任務(wù)隊列都為空了,此次循環(huán)自動結(jié)束,執(zhí)行此回調(diào),輸出10 console.log('10'); }) new Promise(function (resolve) { //13. 執(zhí)行此promise的同步任務(wù),輸出11,狀態(tài)改變 console.log('11'); resolve(); }).then(function () {//14.檢測到then異步方法,加入微任務(wù)隊列 console.log('12');//15.取出微任務(wù)隊列首項,執(zhí)行此then微任務(wù),輸出12 }) })
運行結(jié)果:
此過程步驟詳解:
- 首先進入主線程,檢測到log只是普通函數(shù),壓入執(zhí)行棧,輸出1;
- 檢測到setTimeout為特殊的異步方法(macrotask),將其交由其他內(nèi)核模塊處理,setTimeout的回調(diào)函數(shù)被放入
宏任務(wù)(macrotask)
隊列中; - 檢測到promise對象以及其中的resolve是一般的方法,將其同步任務(wù)壓入執(zhí)行棧,輸出7,并且狀態(tài)改變?yōu)閞essolve;
- 檢測到剛才的promise對象的then方法是異步方法,將其交由其他內(nèi)核模塊處理,回調(diào)函數(shù)被放入
微任務(wù)(microtask)
隊列中; - 又檢測到一個setTimeout為特殊的異步方法,其回調(diào)函數(shù)被放入
宏任務(wù)(macrotask)
隊列中; - 此時,主線程空了,開始從任務(wù)隊列中取,取出 微任務(wù)隊列首項,也就是第一個promise的then方法的回調(diào),執(zhí)行,輸出8;
- 檢查此時微任務(wù)隊列為空,取出宏任務(wù)隊列首項,也就是第一個setTimeOut,執(zhí)行其回調(diào)函數(shù),輸出2;
- 在它的回調(diào)中碰到一個promise,執(zhí)行其同步任務(wù),輸出4,狀態(tài)改變;
- 然后檢測到then,同上,加入到微任務(wù)隊列;
- 取出微任務(wù)隊列首項到主線程執(zhí)行,也就是剛才的then,輸出5;
- 此次循環(huán)結(jié)束,在下一個宏任務(wù)開始之前,調(diào)用第一個process.nextTick()的回調(diào),輸出6;
- 開始下一個宏任務(wù),取出宏任務(wù)隊列首項,也就是第二個setTimeout的回調(diào),將其壓入執(zhí)行棧,輸出9;
- 然后將里面的promise對象的同步任務(wù)壓入執(zhí)行棧,輸出11,狀態(tài)改為resolve;
- 這時又檢測到異步then方法,同上,將其回調(diào)加入 微任務(wù)隊列;
- 取出微任務(wù)隊列首項,也就是剛才的then回調(diào),輸出12;
- 此次循環(huán)結(jié)束,在下一次宏任務(wù)開始之前執(zhí)行,process.nextTick()的回調(diào),輸出3;
- 此時發(fā)現(xiàn) 任務(wù)隊列和主線程都空了,此次事件循環(huán)自動結(jié)束,執(zhí)行最后一個process.nextTick()的回調(diào),輸出10;
結(jié)束!趁著靈光乍現(xiàn)的時候,噼里啪啦趕緊記錄下來,后面再檢查檢查是否有問題,也歡迎各位指出我的錯誤。
再來分析一個簡單的例子:
console.log('0'); setTimeout(() => { console.log('1'); new Promise(function(resolve) { console.log('2'); resolve(); }).then(()=>{ console.log('3'); }) new Promise(resolve => { console.log('4'); for(let i=0;i<9;i++){ i == 7 && resolve(); } console.log('5'); }).then(() => { console.log('6'); }) })
- 進入主線程,檢測到log為普通函數(shù),壓入執(zhí)行棧,輸出0;
- 檢測到setTimeOut是特殊的異步方法,交給其他模塊處理,其回調(diào)函數(shù)加入 宏任務(wù)(macrotask)隊列;
- 此時主線程中已經(jīng)沒有任務(wù),開始從任務(wù)隊列中取;
- 發(fā)現(xiàn)為任務(wù)隊列為空,則取出宏任務(wù)隊列首項,也就是剛才的定時器的回調(diào)函數(shù);
- 執(zhí)行其中的同步任務(wù),輸出1;
- 檢測到promise及其resolve方法是一般的方法,壓入執(zhí)行棧,輸出2,狀態(tài)改變?yōu)閞esolve;
- 檢測到這個promise的then方法是異步方法,將其回調(diào)函數(shù)加入 微任務(wù)隊列;
- 緊接著又檢測到一個promise,執(zhí)行其中的同步任務(wù),輸出4,5,狀態(tài)改變?yōu)閞esolve;
- 然后將它的then異步方法加入微任務(wù)隊列;
- 執(zhí)行微任務(wù)隊列首項,也就是第一個promise的then,輸出3;
- 再取出為任務(wù)隊列首項,也就是第二個promise的then,輸出6;
- 此時主線程和任務(wù)隊列都為空,執(zhí)行完畢;
代碼運行結(jié)果:
到此這篇關(guān)于JavaScript 關(guān)于事件循環(huán)機制的刨析的文章就介紹到這了,更多相關(guān)JavaScript 事件循環(huán)機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js取0-9隨機取4個數(shù)不重復(fù)的數(shù)字代碼實例
這篇文章主要介紹了js取0-9隨機取4個數(shù)不重復(fù)的數(shù)字,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03JavaScript Math.floor方法(對數(shù)值向下取整)
這篇文章主要介紹了Math.floor 方法用于對數(shù)值向下取整,即得到小于或等于該數(shù)值的最大整數(shù),需要的朋友可以參考下2015-01-01escape、encodeURI、encodeURIComponent等方法的區(qū)別比較
escape、encodeURI、encodeURIComponent等方法的區(qū)別比較...2006-12-12javascript處理表單示例(javascript提交表單)
這篇文章主要介紹了javascript處理表單示例,處理 各種表單, 以及鏈接,按鈕的通用組件,需要的朋友可以參考下2014-04-04javascript indexOf方法、lastIndexOf 方法和substring 方法
indexOf() 方法可返回某個指定的字符串值在字符串中首次出現(xiàn)的位置。2009-03-03