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