event loop、进程和线程、任务队列
本文原鏈接:https://cloud.tencent.com/developer/article/1106531
https://cloud.tencent.com/developer/article/1372207
- JavaScript是單線程
- 執(zhí)行棧、任務(wù)隊(duì)列
- 同步任務(wù)、異步任務(wù)、宏任務(wù)、微任務(wù)
- setTimeout()、setInterval()
- Promise
- process.nextTick
- setImmediate
- 優(yōu)先級(jí)
- 代碼解析
- 參考資料
先看段代碼:
console.log(1); setTimeout(function () { console.log(2); new Promise(function (resolve, reject) { console.log(3); resolve(); console.log(4); }).then(function () { console.log(5); }); }); function fn() { console.log(6); setTimeout(function () { console.log(7); }, 50); } new Promise(function (resolve, reject) { console.log(8); resolve(); console.log(9); }).then(function () { console.log(10); }); fn(); console.log(11); // 以下代碼需要在 node 環(huán)境中執(zhí)行 process.nextTick(function () { console.log(12); }); setImmediate(function () { console.log(13); });思考一下,能給出準(zhǔn)確的輸出順序嗎?
下面我們一個(gè)一個(gè)的來(lái)了解 Event Loop 相關(guān)的知識(shí)點(diǎn),最后再一步一步分析出本段代碼最后的輸出順序。
JavaScript是單線程
首先我們先了解下進(jìn)程和線程的概念和關(guān)系:
- 進(jìn)程:?運(yùn)行的程序就是一個(gè)進(jìn)程,比如你正在運(yùn)行的瀏覽器,它會(huì)有一個(gè)進(jìn)程。
- 線程:?程序中獨(dú)立運(yùn)行的代碼段。一個(gè)進(jìn)程?由單個(gè)或多個(gè)?線程?組成,線程是負(fù)責(zé)執(zhí)行代碼的。
我們都知道 JavaScript 是單線程的,那么既然有單線程就有多線程,首先看看單線程與多線程的區(qū)別:
- 單線程:?從頭執(zhí)行到尾,一行一行執(zhí)行,如果其中一行代碼報(bào)錯(cuò),那么剩下代碼將不再執(zhí)行。同時(shí)容易代碼阻塞。
- 多線程:?代碼運(yùn)行的環(huán)境不同,各線程獨(dú)立,互不影響,避免阻塞。
那為什么JavaScript是單線程的呢?
JavaScript 的單線程,與它的用途有關(guān)。作為瀏覽器腳本語(yǔ)言,JavaScript 的主要用途是與用戶互動(dòng),以及操作 DOM。這決定了它只能是單線程,否則會(huì)帶來(lái)很復(fù)雜的同步問(wèn)題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè) DOM 節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)呢?
所以,為了避免復(fù)雜性,從一誕生,JavaScript 就是單線程,這已經(jīng)成了這門語(yǔ)言的核心特征,將來(lái)也不會(huì)改變。
為了利用多核 CPU 的計(jì)算能力,HTML5 提出 Web Worker 標(biāo)準(zhǔn),允許 JavaScript 腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作 DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒(méi)有改變 JavaScript 單線程的本質(zhì)。
執(zhí)行棧、任務(wù)隊(duì)列
上圖中,主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API,它們?cè)?#34;任務(wù)隊(duì)列"中加入各種事件(DOM Event,ajax,setTimeout…)。只要棧中的代碼執(zhí)行完畢,主線程就會(huì)去讀取任務(wù)隊(duì)列,依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
堆(heap):
對(duì)象被分配在一個(gè)堆中,即用以表示一個(gè)大部分非結(jié)構(gòu)化的內(nèi)存區(qū)域。
執(zhí)行棧(stack):
運(yùn)行同步代碼。執(zhí)行棧中的代碼(同步任務(wù)),總是在讀取"任務(wù)隊(duì)列"(異步任務(wù))之前執(zhí)行。
任務(wù)隊(duì)列(callback queue):
"任務(wù)隊(duì)列"是一個(gè)事件的隊(duì)列(也可以理解成消息的隊(duì)列),IO設(shè)備完成一項(xiàng)任務(wù),就在"任務(wù)隊(duì)列"中添加一個(gè)事件,表示相關(guān)的異步任務(wù)可以進(jìn)入"執(zhí)行棧"了。主線程讀取"任務(wù)隊(duì)列",就是讀取里面有哪些事件。
"任務(wù)隊(duì)列"中的事件,除了IO設(shè)備的事件以外,還包括一些用戶產(chǎn)生的事件(比如鼠標(biāo)點(diǎn)擊、頁(yè)面滾動(dòng)等等)。只要指定過(guò)回調(diào)函數(shù),這些事件發(fā)生時(shí)就會(huì)進(jìn)入"任務(wù)隊(duì)列",等待主線程讀取。
所謂"回調(diào)函數(shù)"(callback),就是那些會(huì)被主線程掛起來(lái)的代碼。異步任務(wù)必須指定回調(diào)函數(shù),當(dāng)主線程開(kāi)始執(zhí)行異步任務(wù),就是執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
"任務(wù)隊(duì)列"是一個(gè)先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),排在前面的事件,優(yōu)先被主線程讀取。主線程的讀取過(guò)程基本上是自動(dòng)的,只要執(zhí)行棧一清空,"任務(wù)隊(duì)列"上第一位的事件就自動(dòng)進(jìn)入主線程。但是,由于存在后文提到的"定時(shí)器"功能,主線程首先要檢查一下執(zhí)行時(shí)間,某些事件只有到了規(guī)定的時(shí)間,才能返回主線程。
同步任務(wù)、異步任務(wù)、宏任務(wù)、微任務(wù)
單線程就意味著,所有任務(wù)需要排隊(duì),前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)。如果前一個(gè)任務(wù)耗時(shí)很長(zhǎng),后一個(gè)任務(wù)就不得不一直等著。
如果排隊(duì)是因?yàn)橛?jì)算量大,CPU忙不過(guò)來(lái),倒也算了,但是很多時(shí)候CPU是閑著的,因?yàn)镮O設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來(lái),再往下執(zhí)行。
JavaScript語(yǔ)言的設(shè)計(jì)者意識(shí)到,這時(shí)主線程完全可以不管IO設(shè)備,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果,再回過(guò)頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。
于是,廣義上將 JavaScript 所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);異步任務(wù)指的是,不進(jìn)入主線程、而進(jìn)入"任務(wù)隊(duì)列"(task queue)的任務(wù),只有"任務(wù)隊(duì)列"通知主線程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行。
具體來(lái)說(shuō),異步執(zhí)行的運(yùn)行機(jī)制如下(同步執(zhí)行也是如此,因?yàn)樗梢员灰暈闆](méi)有異步任務(wù)的異步執(zhí)行):
(1)所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)"執(zhí)行棧"(execution context stack); (2)主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件; (3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)取出"任務(wù)隊(duì)列"中事件所對(duì)應(yīng)的回調(diào)函數(shù)進(jìn)入"執(zhí)行棧",開(kāi)始執(zhí)行; (4)主線程不斷重復(fù)上面的第三步。除了廣義上的定義,我們可以將任務(wù)進(jìn)行更精細(xì)的定義,分為宏任務(wù)與微任務(wù):
- 宏任務(wù)(macro-task):?包括整體代碼script,setTimeout,setInterval,ajax,dom操作
- 微任務(wù)(micro-task):?Promise
具體來(lái)說(shuō),宏任務(wù)與微任務(wù)執(zhí)行的運(yùn)行機(jī)制如下:
(1)首先,將"執(zhí)行棧"最開(kāi)始的所有同步代碼(宏任務(wù))執(zhí)行完成; (2)檢查是否有微任務(wù),如有則執(zhí)行所有的微任務(wù); (3)取出"任務(wù)隊(duì)列"中事件所對(duì)應(yīng)的回調(diào)函數(shù)(宏任務(wù))進(jìn)入"執(zhí)行棧"并執(zhí)行完成; (4)再檢查是否有微任務(wù),如有則執(zhí)行所有的微任務(wù); (5)主線程不斷重復(fù)上面的(3)(4)步。以上兩種運(yùn)行機(jī)制,主線程都從"任務(wù)隊(duì)列"中讀取事件,這個(gè)過(guò)程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為?Event Loop(事件循環(huán))。
setTimeout()、setInterval()
setTimeout() 和 setInterval() 這兩個(gè)函數(shù),它們的內(nèi)部運(yùn)行機(jī)制完全一樣,區(qū)別在于前者指定的代碼是一次性執(zhí)行,后者則為反復(fù)執(zhí)行。
setTimeout() 和 setInterval() 產(chǎn)生的任務(wù)是?異步任務(wù),也屬于?宏任務(wù)。
setTimeout() 接受兩個(gè)參數(shù),第一個(gè)是回調(diào)函數(shù),第二個(gè)是推遲執(zhí)行的毫秒數(shù)。setInterval() 接受兩個(gè)參數(shù),第一個(gè)是回調(diào)函數(shù),第二個(gè)是反復(fù)執(zhí)行的毫秒數(shù)。
如果將第二個(gè)參數(shù)設(shè)置為0或者不設(shè)置,意思?并不是立即執(zhí)行,而是指定某個(gè)任務(wù)在主線程最早可得的空閑時(shí)間執(zhí)行,也就是說(shuō),盡可能早得執(zhí)行。它在"任務(wù)隊(duì)列"的尾部添加一個(gè)事件,因此要等到同步任務(wù)和"任務(wù)隊(duì)列"現(xiàn)有的事件都處理完,才會(huì)得到執(zhí)行。
所以說(shuō),setTimeout() 和 setInterval() 第二個(gè)參數(shù)設(shè)置的時(shí)間并不是絕對(duì)的,它需要根據(jù)當(dāng)前代碼最終執(zhí)行的時(shí)間來(lái)確定的,簡(jiǎn)單來(lái)說(shuō),如果當(dāng)前代碼執(zhí)行的時(shí)間(如執(zhí)行200ms)超出了推遲執(zhí)行(setTimeout(fn, 100))或反復(fù)執(zhí)行的時(shí)間(setInterval(fn, 100)),那么setTimeout(fn, 100) 和 setTimeout(fn, 0) 也就沒(méi)有區(qū)別了,setInterval(fn, 100) 和 setInterval(fn, 0) 也就沒(méi)有區(qū)別了。
Promise
Promise 相對(duì)來(lái)說(shuō)就比較特殊了,在 new Promise() 中傳入的回調(diào)函數(shù)是會(huì)?立即執(zhí)行?的,但是它的?then()?方法是在?執(zhí)行棧之后,任務(wù)隊(duì)列之前?執(zhí)行的,它屬于?微任務(wù)。
process.nextTick
process.nextTick 是 Node.js 提供的一個(gè)與"任務(wù)隊(duì)列"有關(guān)的方法,它產(chǎn)生的任務(wù)是放在?執(zhí)行棧的尾部,并不屬于?宏任務(wù)?和?微任務(wù),因此它的任務(wù)?總是發(fā)生在所有異步任務(wù)之前。
setImmediate
setImmediate 是 Node.js 提供的另一個(gè)與"任務(wù)隊(duì)列"有關(guān)的方法,它產(chǎn)生的任務(wù)追加到"任務(wù)隊(duì)列"的尾部,它和?setTimeout(fn, 0)?很像,但優(yōu)先級(jí)都是 setTimeout 優(yōu)先于 setImmediate。
有時(shí)候,setTimeout 的執(zhí)行順序會(huì)在 setImmediate 的前面,有時(shí)候會(huì)在 setImmediate 的后面,這并不是 node.js 的 bug,這是因?yàn)殡m然 setTimeout 第二個(gè)參數(shù)設(shè)置為0或者不設(shè)置,但是 setTimeout 源碼中,會(huì)指定一個(gè)具體的毫秒數(shù)(node為1ms,瀏覽器為4ms),而由于當(dāng)前代碼執(zhí)行時(shí)間受到執(zhí)行環(huán)境的影響,執(zhí)行時(shí)間有所起伏,如果當(dāng)前執(zhí)行的代碼小于這個(gè)指定的值時(shí),setTimeout 還沒(méi)到推遲執(zhí)行的時(shí)間,自然就先執(zhí)行 setImmediate 了,如果當(dāng)前執(zhí)行的代碼超過(guò)這個(gè)指定的值時(shí),setTimeout 就會(huì)先于 setImmediate 執(zhí)行。
優(yōu)先級(jí)
通過(guò)上面的介紹,我們就可以得出一個(gè)代碼執(zhí)行的優(yōu)先級(jí):
同步代碼(宏任務(wù)) > process.nextTick > Promise(微任務(wù))> setTimeout(fn)、setInterval(fn)(宏任務(wù))> setImmediate(宏任務(wù))> setTimeout(fn, time)、setInterval(fn, time),其中time>0
代碼解析
回到開(kāi)頭給出的代碼,我們來(lái)一步一步解析:
運(yùn)行"執(zhí)行棧"中的代碼:
console.log(1); // setTimeout(function () { // 作為宏任務(wù),暫不執(zhí)行,放到任務(wù)隊(duì)列中(1) // console.log(2); // // new Promise(function (resolve, reject) { // console.log(3); // resolve(); // console.log(4); // }).then(function () { // console.log(5); // }); // }); function fn() { console.log(6); //setTimeout(function () { // 作為宏任務(wù),暫不執(zhí)行,放到任務(wù)隊(duì)列中(3) // console.log(7); //}, 50); } new Promise(function (resolve, reject) { console.log(8); resolve(); console.log(9); }) // .then(function () { // 作為微任務(wù),暫不執(zhí)行 // console.log(10); // }); fn(); console.log(11); process.nextTick(function () { console.log(12); }); // setImmediate(function () { // 作為宏任務(wù),暫不執(zhí)行,放到任務(wù)隊(duì)列中(2) // console.log(13); // });此時(shí)輸出為:1 8 9 6 11 12
運(yùn)行微任務(wù):
new Promise(function (resolve, reject) { // console.log(8); // 已執(zhí)行 // resolve(); // 已執(zhí)行 // console.log(9); // 已執(zhí)行 }) .then(function () { console.log(10); });此時(shí)輸出為:10
讀取"任務(wù)隊(duì)列"的回調(diào)函數(shù)到"執(zhí)行棧":
setTimeout(function () { console.log(2); new Promise(function (resolve, reject) { console.log(3); resolve(); console.log(4); }) //.then(function () { // 作為微任務(wù),暫不執(zhí)行 // console.log(5); //}); });此時(shí)輸出為:2 3 4
再運(yùn)行微任務(wù):
setTimeout(function () { // console.log(2); // 已執(zhí)行 new Promise(function (resolve, reject) { // console.log(3); // 已執(zhí)行 // resolve(); // 已執(zhí)行 // console.log(4); // 已執(zhí)行 }) .then(function () { console.log(5); }); });此時(shí)輸出為:5
再讀取"任務(wù)隊(duì)列"的回調(diào)函數(shù)到"執(zhí)行棧":
setImmediate(function () { console.log(13); });此時(shí)輸出為:13
運(yùn)行微任務(wù):
無(wú)
再讀取"任務(wù)隊(duì)列"的回調(diào)函數(shù)到"執(zhí)行棧":
// function fn() { // 已執(zhí)行// console.log(6); // 已執(zhí)行setTimeout(function () { console.log(7); }, 50); // }此時(shí)輸出為:7
運(yùn)行微任務(wù):
無(wú)
綜上,最終的輸出順序是:1 8 9 6 11 12 10 2 3 4 5 13 7
?
轉(zhuǎn)載于:https://www.cnblogs.com/leftJS/p/11070104.html
總結(jié)
以上是生活随笔為你收集整理的event loop、进程和线程、任务队列的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Python3.7.1学习(五) 将列表
- 下一篇: 如何在MFC客户端调用COM DLL