摘要:概述本篇主要介紹的運(yùn)行機(jī)制單線程事件循環(huán)結(jié)論先在中利用運(yùn)行至完成和非阻塞完成單線程下異步任務(wù)的處理就是先處理主模塊主線程上的同步任務(wù)再處理異步任務(wù)異步任務(wù)使用事件循環(huán)機(jī)制完成調(diào)度涉及的內(nèi)容有單線程事件循環(huán)同步執(zhí)行異步執(zhí)行定時器的事件循環(huán)開始
1.概述
本篇主要介紹JavaScript的運(yùn)行機(jī)制:單線程事件循環(huán)(Event Loop).
結(jié)論先: 在JavaScript中, 利用運(yùn)行至完成和非阻塞IO 完成單線程下異步任務(wù)的處理. 就是先處理主模塊(主線程)上的同步任務(wù), 再處理異步任務(wù). 異步任務(wù)使用事件循環(huán)機(jī)制完成調(diào)度.
涉及的內(nèi)容有: 單線程, 事件循環(huán), 同步執(zhí)行, 異步執(zhí)行, 定時器, nodeJS的事件循環(huán)
開始之前, 先看下面的代碼, 給出結(jié)果:
// 當(dāng)前時間 console.log("A: " + new Date()); // 1秒(1000毫秒)后執(zhí)行的定時器 // 異步執(zhí)行的代碼 setTimeout(function() { console.log("B: " + new Date()); }, 1000); // 循環(huán)3秒(3000毫秒) var end = Date.now() + 3000; while(Date.now() < end) { } // 當(dāng)前時間 console.log("C: " + new Date());
在瀏覽器中的結(jié)果為(chrome-50.0.2661.102):
A: Thu May 25 2017 13:48:26 GMT+0800 (中國標(biāo)準(zhǔn)時間) C: Thu May 25 2017 13:48:29 GMT+0800 (中國標(biāo)準(zhǔn)時間) B: Thu May 25 2017 13:48:29 GMT+0800 (中國標(biāo)準(zhǔn)時間)
在NodeJS(v7.7.2 win-x64)中的結(jié)果為:
>node scriptsasync.js A: Thu May 25 2017 13:50:55 GMT+0800 (中國標(biāo)準(zhǔn)時間) C: Thu May 25 2017 13:50:58 GMT+0800 (中國標(biāo)準(zhǔn)時間) B: Thu May 25 2017 13:50:58 GMT+0800 (中國標(biāo)準(zhǔn)時間)
tip: 瀏覽器下和NodeJS結(jié)果一致.
分析上面的代碼與結(jié)果, 注意的要點(diǎn):
雖然設(shè)置的定時器為1秒后執(zhí)行, 但實(shí)際的執(zhí)行時間在3秒以后, 看結(jié)果中B:的輸出, 在A:的3秒后.
B:的輸出在C:的輸出之后. 可見, 雖然在while循環(huán)后, 時間已經(jīng)到了定時器代碼需要執(zhí)行的時間, 但并沒有立即執(zhí)行, 而是等到了console.log("C: ")執(zhí)行完, 再執(zhí)行的定時器的代碼.
本篇就是說明為什么會出現(xiàn)以上的現(xiàn)象. 下面請一步步的看.
2.單線程單線程, 指的是JavaScript在一個時間僅處理一個任務(wù). 就是JavaScript在執(zhí)行時, 存在一個執(zhí)行隊(duì)列, 依次執(zhí)行隊(duì)列中的任務(wù), 不能同時執(zhí)行多個任務(wù).
單線程的優(yōu)勢, 也是JavaScript選擇單線程的原因是:
1, 降低處理復(fù)雜性, 簡化開發(fā). 例如不用考慮死鎖, 競爭機(jī)制等.
2, 作為用于處理與用戶互動的腳本語言, 可以更加容易地處理狀態(tài)同步的問題(想想考慮用戶操作的不確定性).
3, JavaScript核心維護(hù)人員自身的設(shè)計(jì)與理解.
4, 越簡單越容易推廣, 快速上手.
除了優(yōu)勢, 單線程有明顯的劣勢, 就是并發(fā)處理能力, 因?yàn)閱尉€程處理下所有的任務(wù)就要排隊(duì)處理. 但是如果排在前面的任務(wù)處理很耗時, 那就導(dǎo)致后面的任務(wù)一直處于等待狀態(tài). 如果前面的任務(wù)出處于滿載運(yùn)行狀態(tài)還可以, 但是如果前面的任務(wù)處于IO等待狀態(tài)呢? 就會導(dǎo)致CPU處理資源的浪費(fèi).
思考, 前面的是AJAX任務(wù), 后邊是其他任務(wù). AJAX任務(wù)需要等待網(wǎng)絡(luò)請求響應(yīng)結(jié)束, 才能處理, 此時前面的AJAX任務(wù)就處于IO等待狀態(tài). 從而導(dǎo)致后面的任務(wù)也執(zhí)行不了, 造成了單線程下的資源浪費(fèi). (CPU沒有辦法高速運(yùn)轉(zhuǎn), 處于空閑狀態(tài)).
在此情況下, 完全可以掛起前面的AJAX任務(wù)(掛起等待AJAX的響應(yīng)結(jié)果), 先執(zhí)行后面的任務(wù). 等后面的任務(wù)處理完畢后, 再看前面的AJAX任務(wù)是否得到了IO結(jié)果, 如果有結(jié)果了, 在翻回來處理即可. 這種處理方式, 就是異步方式.
3.同步任務(wù)和異步任務(wù)單線程的JavaScript為了更好利用CPU的性能, 將執(zhí)行的任務(wù)設(shè)計(jì)為: 同步任務(wù)和異步任務(wù), 兩類.
同步任務(wù)(synchronous task), 就是需要一個個順序執(zhí)行的任務(wù), 不能跳過, 執(zhí)行完前一個才能執(zhí)行后一個. 我們稱之為在主模塊(主線程)執(zhí)行的任務(wù).
異步任務(wù)(asynchronous task), 指的是被掛起執(zhí)行的任務(wù), 在系統(tǒng)內(nèi)部處于等待IO處理結(jié)果狀態(tài), 一旦處理完畢, 記錄下來, 等待后續(xù)處理. 需要事件循環(huán)處理的任務(wù). 上面示例中的AJAX任務(wù)就是異步任務(wù).
你應(yīng)該會想, JavaScript不是單線程么, 怎么還能異步處理呢?
是這樣的, JavaScript的單線程, 指的是在JavaScript語言(語法)層面是單線程的. 而內(nèi)部的執(zhí)行, 還是可以利用到處理器多線程和操作系統(tǒng)的任務(wù)調(diào)度的, 在后臺處理我們的異步任務(wù). 當(dāng)操作在后臺被處理完成后(例如ajax接收完畢了服務(wù)器的響應(yīng)), 操作系統(tǒng)將結(jié)果告知給JavaScript, 并最終被JavaScript執(zhí)行.
JavaScript是如何調(diào)度這些同步任務(wù)和異步任務(wù)的呢?
就涉及到了, 本文的重點(diǎn): 任務(wù)隊(duì)列 和 事件循環(huán), 執(zhí)行棧.
如圖(邏輯概述圖)所示:
執(zhí)行如下:
step1, 同步任務(wù)直接放入到主模塊(主線程)任務(wù)隊(duì)列執(zhí)行. 異步任務(wù)掛起后臺執(zhí)行, 等待IO事件完成或行為事件被觸發(fā).
step2, 系統(tǒng)后臺執(zhí)行異步任務(wù), 如果某個異步任務(wù)事件發(fā)生(或者是行為事件被觸發(fā)), 則將該任務(wù)push到任務(wù)隊(duì)列中, 每個任務(wù)會對應(yīng)一個回調(diào)函數(shù)進(jìn)行處理. 這個步驟在后臺一直執(zhí)行, 因?yàn)榫筒粩嘤惺录挥|發(fā), IO不斷完成, 任務(wù)被不斷的加入到任務(wù)隊(duì)列中.
step3, 執(zhí)行任務(wù)隊(duì)列中的任務(wù). 任務(wù)的具體執(zhí)行是在執(zhí)行棧中完成的. 當(dāng)運(yùn)行棧中一個任務(wù)的基本運(yùn)行單元(稱之為Frame, 楨)全部執(zhí)行完畢后, 去讀取任務(wù)隊(duì)列中的下一個任務(wù), 繼續(xù)執(zhí)行. 是一個循環(huán)的過程. 處理一個任務(wù)隊(duì)列中的任務(wù), 稱之為一個tick.
注意, step3, 是一個循環(huán)的過程, 這就是事件循環(huán). 循環(huán)執(zhí)行任務(wù)隊(duì)列中已經(jīng)發(fā)生的事件對應(yīng)的任務(wù).
再參考開始的代碼, 我們可以知道:
// A:當(dāng)前時間 // 同步代碼, 直接進(jìn)入任務(wù)隊(duì)列 console.log("A: " + new Date()); // B:1秒(1000毫秒)后執(zhí)行的定時器 // 異步代碼, 等待到時事件發(fā)生, 才會進(jìn)入任務(wù)隊(duì)列 setTimeout(function() { console.log("B: " + new Date()); }, 1000); // 循環(huán)3秒(3000毫秒), // 同步代碼, 直接進(jìn)入任務(wù)隊(duì)列 var end = Date.now() + 3000; // 同步代碼, 直接進(jìn)入任務(wù)隊(duì)列 while(Date.now() < end) { } // C:當(dāng)前時間 // 同步代碼, 直接進(jìn)入任務(wù)隊(duì)列 console.log("C: " + new Date());
也就意味著, 此時, log(A), while, log(C) 三個任務(wù), 已經(jīng)進(jìn)入到了任務(wù)隊(duì)列中. 而setTimeout是異步任務(wù)(與AJAX一致)在等待事件發(fā)生(到時事件). 于此同時, JavaScript開始處理任務(wù)隊(duì)列. 隊(duì)列是先進(jìn)先出, 需要依次處理. 所以, 即時當(dāng)前已經(jīng)到1s了, 事件發(fā)生, 也僅僅是將該任務(wù)push入任務(wù)隊(duì)列而已(并沒有立即執(zhí)行回調(diào)函數(shù)). 當(dāng)將setTimeout入隊(duì)列時, log(C)已經(jīng)在隊(duì)列中了, 因此, setTimeout的log(B), 會在log(B)后執(zhí)行. 這就是輸出了: A, C, B的原因. 如下圖(邏輯概述圖)所示:
由任務(wù)隊(duì)列可知, 輸出為: A, C, B順序.
JavaScript提供了可以操作定時器的函數(shù), setTimeout()和setInterval. 在NodeJS中還有setImmediate().
setTimeout(), 定時執(zhí)行setTimeout(callback, timer), 多久(毫秒計(jì))后執(zhí)行, 常規(guī)用法已經(jīng)演示.
需要提醒大家的是, setTimetout()是延時觸發(fā), 而不是即時觸發(fā). 指的是, 在有機(jī)會處理計(jì)時器事件時, 優(yōu)先處理最先到時的計(jì)時器程序. 而不是時間到立即處理. 因?yàn)槭菃尉€程, 需要先處理當(dāng)前的任務(wù), 例如主模塊中的任務(wù)(同步任務(wù)).
實(shí)操中還有一個setTimeout(callback, 0)的用法, 表示立即加入到任務(wù)隊(duì)列. 但是注意, 并不是在執(zhí)行setTimeout的時候, 就加入隊(duì)列了, 而是當(dāng)全部的同步任務(wù)入隊(duì)列后, 立即加入到任務(wù)隊(duì)列, 也就意味著同步任務(wù)之后第一個執(zhí)行. 但據(jù)說這個值內(nèi)部執(zhí)行時有一個最小值, 4ms.
上面的代碼, 將時間改為0, 測試結(jié)果還是A, C, B. 不會因?yàn)橄葓?zhí)行的setTimeout()而就將任務(wù)先執(zhí)行.
// 異步代碼, 等待到時事件發(fā)生, 才會進(jìn)入任務(wù)隊(duì)列 setTimeout(function() { console.log("B: " + new Date()); }, 0);
未發(fā)生的定時器, 可以使用clearTimeout()方法清除.
setInterval(), 循環(huán)執(zhí)行.setInterval(callback, timer) 與setTimeout()相似, 不過是在callback執(zhí)行完畢后, 再次設(shè)置了計(jì)時器. 不再贅述.
6.NodeJS中的事件循環(huán)NodeJS的事件循環(huán)模型比瀏覽器更為復(fù)雜些.
如下圖所示(引用自NodeJS官方文檔), 事件循環(huán), 按照下圖的順序調(diào)用事件.
由于出現(xiàn)了不同的事件循環(huán)段, 例如 timer, check, 出現(xiàn)了額外的控制定時器方法.
邏輯含義上講, 與setTimeout(callback, 0)一致. 都是立即執(zhí)行. 在NodeJS中setImmediate()存在的主要場景就是, 在異步IO調(diào)用中, 如果同時使用setImmediate()和settimeout(), 可以保證, setImmediate()先于所有的setTimeout()執(zhí)行.
如下代碼: (引用自NodeJS官方文檔)
var fs = require("fs"); // 異步文件IO fs.readFile(__filename, () => { setTimeout(function timeout () { console.log("timeout"); },0); setImmediate(function immediate () { console.log("immediate"); }); });
以上代碼的執(zhí)行結(jié)果, 一定是:
>node scriptsasync-node.js immediate timeout 這是因?yàn)? 根據(jù)NodeJS的事件循環(huán)處理順序, 處理完IO后, 需要處理check, 而setImmediate()就是check中的事件. 因此先處理.
但上面的代碼如果沒有在異步IO中調(diào)用, 在主模塊(主線程)中調(diào)用, 則順序不一定, 由操作系統(tǒng)調(diào)度決定!
process.nextTick(callback)tick, 就是一個事件循環(huán)周期. 在prcess.nextTick()中設(shè)置的異步callback會在當(dāng)前事件循環(huán)周期結(jié)束, 下一個事件循環(huán)周期開始前執(zhí)行.
像是一個插入的tick. 生成了一個新的周期. 說白了, 是一個插隊(duì)行為.
因此, 在時間上看, 一定先于settimeout(callback, 0)和setImmediate()執(zhí)行. 通常用來處理在下一個事件周期(異步任務(wù))前, 必須要處理好的任務(wù). 常見的有, 處理錯誤, 回收資源, 和 重新執(zhí)行存在錯誤的操作等.
測試一下執(zhí)行時機(jī):
setTimeout(function timeout () { console.log("timeout"); },0); setImmediate(function immediate () { console.log("immediate"); }); process.nextTick(function immediate () { console.log("nickTick"); });
結(jié)果為:
>node scriptsasync-node.js nextTick timeout immediate
可見, nextTick先發(fā)生.
注意, 在NodeJS中, nexttick并不是一個特殊的定時器.
注意, 由于nextTick()會插隊(duì)執(zhí)行, 因此, NodeJS限制了nextTick()遞歸調(diào)用的深度. 防止IO處理饑餓.一直在處理nextTick(). 由于該原因, 遞歸時, NodeJS建議使用setImmediate()完成.
process.nextTick, 永遠(yuǎn)先執(zhí)行.
setImmediate和setTimeout, 那個先到時那個先執(zhí)行. 如果同時, 則由系統(tǒng)調(diào)度負(fù)責(zé).
在JavaScript中, 利用運(yùn)行至完成和非阻塞IO 完成單線程下異步任務(wù)的處理. 就是先處理主模塊(主線程)上的同步任務(wù), 再處理異步任務(wù). 異步任務(wù)使用事件循環(huán)機(jī)制完成調(diào)度.
參考:
NodeJS文檔, https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop, http://www.ruanyifeng.com/blog/2014/10/event-loop.html 樸靈 深入淺出Node.js http://www.infoq.com/cn/master-nodejs8.結(jié)語
以上就是本人對事件循環(huán)的理解. 一家之言, 歡迎討論拍磚!
更多內(nèi)容, 可以關(guān)注, 微信公眾號, 小韓說理.
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/83187.html
摘要:主線程從任務(wù)隊(duì)列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運(yùn)行機(jī)制又稱為事件循環(huán)。上面也提到,在到達(dá)指定時間時,定時器就會將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊(duì)列尾部。這就是定時器功能。關(guān)于定時器的重要補(bǔ)充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運(yùn)行機(jī)制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
摘要:主線程從任務(wù)隊(duì)列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運(yùn)行機(jī)制又稱為事件循環(huán)。上面也提到,在到達(dá)指定時間時,定時器就會將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊(duì)列尾部。這就是定時器功能。關(guān)于定時器的重要補(bǔ)充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運(yùn)行機(jī)制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
摘要:主線程從任務(wù)隊(duì)列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運(yùn)行機(jī)制又稱為事件循環(huán)。上面也提到,在到達(dá)指定時間時,定時器就會將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊(duì)列尾部。這就是定時器功能。關(guān)于定時器的重要補(bǔ)充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運(yùn)行機(jī)制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
js異步歷史 一個 JavaScript 引擎會常駐于內(nèi)存中,它等待著我們把JavaScript 代碼或者函數(shù)傳遞給它執(zhí)行 在 ES3 和更早的版本中,JavaScript 本身還沒有異步執(zhí)行代碼的能力,引擎就把代碼直接順次執(zhí)行了,異步任務(wù)都是宿主環(huán)境(瀏覽器)發(fā)起的(setTimeout、AJAX等)。 在 ES5 之后,JavaScript 引入了 Promise,這樣,不需要瀏覽器的安排,J...
閱讀 840·2023-04-26 00:13
閱讀 2885·2021-11-23 10:08
閱讀 2463·2021-09-01 10:41
閱讀 2127·2021-08-27 16:25
閱讀 4221·2021-07-30 15:14
閱讀 2375·2019-08-30 15:54
閱讀 873·2019-08-29 16:22
閱讀 2754·2019-08-26 12:13