一、為什么JS語言是單線程
js的單線程和他的用途有關(guān)糊余。作為瀏覽器腳本語言秀又,js的主要用途就是與用戶互動(dòng),以及操作DOM贬芥、BOM吐辙。這決定了它只能是單線程,否則會(huì)有很復(fù)雜的同步問題蘸劈。例如:js同時(shí)有兩個(gè)線程昏苏,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程在刪除這個(gè)節(jié)點(diǎn)威沫,此時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)贤惯?
因此,為了避免復(fù)雜性棒掠,從誕生以來孵构,JS就是單線程,這是這么語言的核心特征烟很。
為了利用多核cpu的計(jì)算能力颈墅,HTML5提出Web Worker標(biāo)準(zhǔn),允許js腳本創(chuàng)建多個(gè)線程雾袱,但是子線程完全受主線程控制恤筛,且不得操作DOM,所有新標(biāo)準(zhǔn)并沒有改變js單線程的本質(zhì)芹橡。
二毒坛、任務(wù)隊(duì)列
單線程就意味著,任務(wù)得一個(gè)一個(gè)的執(zhí)行僻族,前一個(gè)任務(wù)結(jié)束粘驰,后一個(gè)任務(wù)才能執(zhí)行屡谐。但是當(dāng)前一個(gè)任務(wù)的耗時(shí)很長(zhǎng)述么,就會(huì)阻塞后面的任務(wù)的執(zhí)行。如果能先執(zhí)行后面的短任務(wù)愕掏,在執(zhí)行有了結(jié)果的長(zhǎng)任務(wù)度秘,于是就有了同步任務(wù)和異步任務(wù)。同步任務(wù)是指,在主線程上的任務(wù)剑梳,只有前一個(gè)任務(wù)執(zhí)行完畢唆貌,才能執(zhí)行后一個(gè)任務(wù);異步任務(wù)是指垢乙,不進(jìn)入主線程锨咙,而是在任務(wù)隊(duì)列中的任務(wù),當(dāng)異步任務(wù)有了結(jié)果追逮,就會(huì)在隊(duì)列中添加一個(gè)事件酪刀。當(dāng)主線程的同步任務(wù)都執(zhí)行完成后,再去異步的任務(wù)隊(duì)列中按照從前往后的順序钮孵,執(zhí)行異步任務(wù)添加的事件骂倘,也就是執(zhí)行回調(diào)函數(shù)。如此反復(fù)巴席,便形成一個(gè)事件循環(huán)历涝。
js運(yùn)行機(jī)制:
(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)的異步任務(wù),于是結(jié)束等待狀態(tài)恤煞,進(jìn)入執(zhí)行棧屎勘,開始執(zhí)行。
(4)主線程不斷重復(fù)上面的第三步居扒。
下圖就是主線程和任務(wù)隊(duì)列的示意圖概漱。
只要主線程空了,就會(huì)去讀取"任務(wù)隊(duì)列"喜喂,這就是JavaScript的運(yùn)行機(jī)制瓤摧。這個(gè)過程會(huì)不斷重復(fù)。
補(bǔ)充:異步任務(wù)分為task(宏任務(wù)玉吁,也可稱為macroTask)和microtask(微任務(wù))兩類照弥。
1、一個(gè)線程中进副,
事件循環(huán)是唯一
的这揣,但是任務(wù)隊(duì)列可以擁有多個(gè)
。
2、任務(wù)隊(duì)列又分為macro-task
(宏任務(wù))與micro-task
(微任務(wù))给赞,在最新標(biāo)準(zhǔn)中机打,它們被分別稱為task
與jobs
。
3片迅、macro-task
大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering
残邀。
4、micro-task
大概包括:process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
5柑蛇、setTimeout/Promise
等我們稱之為任務(wù)源
罐旗。而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)。setTimeout, setInterval
是同一個(gè)任務(wù)源唯蝶,因此他們的回調(diào)位于同一個(gè)任務(wù)隊(duì)列中九秀,setImmediate
、I/O
等均為不同的任務(wù)源粘我,他們的回調(diào)也會(huì)放到任務(wù)隊(duì)列中鼓蜒,但不是同一個(gè)任務(wù)隊(duì)列。
js執(zhí)行優(yōu)先級(jí):
1征字、同步代碼(按照代碼順序執(zhí)行都弹,promise
構(gòu)造函數(shù)立即執(zhí)行,屬于同步代碼)匙姜,
任務(wù)隊(duì)列:
2畅厢、所有的微任務(wù)(優(yōu)先級(jí)process.nextTick > promise.then
)
3、其中的一個(gè)任務(wù)隊(duì)列(根據(jù)系統(tǒng)性能的不同氮昧,可能會(huì)導(dǎo)致任務(wù)隊(duì)列的優(yōu)先順序不一樣框杜,姑且與Node的事件循環(huán)一致)
4、所有的微任務(wù)
5袖肥、其中的一個(gè)任務(wù)隊(duì)列
6咪辱、所有的微任務(wù)
7、其中的一個(gè)任務(wù)隊(duì)列(外層任務(wù)可能已經(jīng)執(zhí)行完了椎组,到了內(nèi)部嵌套的其他異步任務(wù))
....等
一個(gè)例子:
console.log('start')
setTimeout(() => {
console.log('setTimeout1');
},0);
const myInterval = setInterval(() => {
console.log('setInterval');
},0)
setTimeout(() => {
console.log('setTimeout2');
Promise.resolve().then(() => {
console.log('promise3');
})
setTimeout(() => {
console.log('setTimeout3');
clearInterval(myInterval);
},0)
},0)
Promise.resolve()
.then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
})
console.log('end');
這段代碼最后的輸出結(jié)果如下:
start
end
promise1
promise2
setTimeout1
setInterval
setTimeout2
promise3
setInterval
setTimeout3
兩個(gè)例子:
console.log('golb1');
setImmediate(function () {
console.log('immediate1');
process.nextTick(function () {
console.log('immediate1_nextTick');
})
new Promise(function (resolve) {
console.log('immediate1_promise'); resolve();
}).then(function () { console.log('immediate1_then') })
setImmediate(()=>{
console.log('immediate2-child')
})
})
setTimeout(function () {
console.log('timeout1');
process.nextTick(function () {
console.log('timeout1_nextTick');
})
new Promise(function (resolve) {
console.log('timeout1_promise'); resolve();
}).then(function () {
console.log('timeout1_then')
})
setTimeout(()=>{
console.log('timeout1-child')
})
})
new Promise(function (resolve) {
console.log('glob1_promise'); resolve();
}).then(function () {
console.log('glob1_then')
})
process.nextTick(function () {
console.log('glob1_nextTick');
})
setImmediate(function () {
console.log('immediate2');
process.nextTick(function () {
console.log('immediate2_nextTick');
})
new Promise(function (resolve) {
console.log('immediate2_promise'); resolve();
}).then(function () { console.log('immediate2_then') })
setImmediate(()=>{
console.log('immediate2-child')
})
})
new Promise(function (resolve) {
console.log('glob2_promise'); resolve();
}).then(function () {
console.log('glob2_then')
})
process.nextTick(function () {
console.log('glob2_nextTick');
})
setTimeout(function () {
console.log('timeout2');
process.nextTick(function () {
console.log('timeout2_nextTick');
})
new Promise(function (resolve) {
console.log('timeout2_promise');
resolve();
}).then(function () {
console.log('timeout2_then')
})
setTimeout(()=>{
console.log('timeout2-child')
})
})
golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then
timeout1-child
timeout2-child
immediate2-child
immediate2-child
三油狂、事件和回調(diào)函數(shù)
"任務(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)擊弦聂、頁面滾動(dòng)等等)鸟辅。只要指定過回調(diào)函數(shù)氛什,這些事件發(fā)生時(shí)就會(huì)進(jìn)入"任務(wù)隊(duì)列"莺葫,等待主線程讀取。
所謂"回調(diào)函數(shù)"(callback)枪眉,就是那些會(huì)被主線程掛起來的代碼捺檬。異步任務(wù)必須指定回調(diào)函數(shù),當(dāng)主線程開始執(zhí)行異步任務(wù)贸铜,就是執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)堡纬。
"任務(wù)隊(duì)列"是一個(gè)先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),排在前面的事件蒿秦,優(yōu)先被主線程讀取烤镐。主線程的讀取過程基本上是自動(dòng)的,只要執(zhí)行棧一清空棍鳖,"任務(wù)隊(duì)列"上第一位的事件就自動(dòng)進(jìn)入主線程
四炮叶、事件循環(huán)(Event Loop)
主線程從"任務(wù)隊(duì)列"中讀取事件,這個(gè)過程是循環(huán)不斷的渡处,所以整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán))镜悉。
上圖中,主線程運(yùn)行的時(shí)候医瘫,產(chǎn)生堆和棧侣肄,棧中的代碼調(diào)用各種外部API,它們?cè)谌蝿?wù)隊(duì)列中加入各種事件(click醇份,load稼锅,done)。只要棧中的代碼執(zhí)行完畢僚纷,主線程就會(huì)去讀取"任務(wù)隊(duì)列"缰贝,依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
執(zhí)行棧中的代碼(同步任務(wù))畔濒,總是在讀取"任務(wù)隊(duì)列"(異步任務(wù))之前執(zhí)行剩晴。
五、定時(shí)器
除了放置異步任務(wù)的事件侵状,"任務(wù)隊(duì)列"還可以放置定時(shí)事件赞弥,即指定某些代碼在多少時(shí)間之后執(zhí)行。這叫做"定時(shí)器"(timer)功能趣兄,也就是定時(shí)執(zhí)行的代碼绽左。
定時(shí)器功能主要由setTimeout()
和setInterval()
這兩個(gè)函數(shù)來完成,它們的內(nèi)部運(yùn)行機(jī)制完全一樣艇潭,區(qū)別在于前者指定的代碼是一次性執(zhí)行拼窥,后者則為反復(fù)執(zhí)行戏蔑。以下主要討論setTimeout()
。
setTimeout()
接受兩個(gè)參數(shù)鲁纠,第一個(gè)是回調(diào)函數(shù)总棵,第二個(gè)是推遲執(zhí)行的毫秒數(shù)。
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
上面代碼的執(zhí)行結(jié)果是1改含,3情龄,2,因?yàn)?code>setTimeout()將第二行推遲到1000毫秒之后執(zhí)行捍壤。
如果將setTimeout()
的第二個(gè)參數(shù)設(shè)為0骤视,就表示當(dāng)前代碼執(zhí)行完(執(zhí)行棧清空)以后,立即執(zhí)行(0毫秒間隔)指定的回調(diào)函數(shù)鹃觉。
setTimeout(function(){console.log(1);}, 0);
console.log(2);
上面代碼的執(zhí)行結(jié)果總是2专酗,1,因?yàn)橹挥性趫?zhí)行完第二行以后盗扇,系統(tǒng)才會(huì)去執(zhí)行"任務(wù)隊(duì)列"中的回調(diào)函數(shù)祷肯。
總之,setTimeout(fn,0)
的含義是粱玲,指定某個(gè)任務(wù)在主線程最早可得的空閑時(shí)間執(zhí)行躬柬,也就是說,盡可能早得執(zhí)行抽减。它在"任務(wù)隊(duì)列"的尾部添加一個(gè)事件允青,因此要等到同步任務(wù)和"任務(wù)隊(duì)列"現(xiàn)有的事件都處理完,才會(huì)得到執(zhí)行卵沉。
HTML5標(biāo)準(zhǔn)規(guī)定了setTimeout()
的第二個(gè)參數(shù)的最小值(最短間隔)颠锉,不得低于4毫秒,如果低于這個(gè)值史汗,就會(huì)自動(dòng)增加琼掠。在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒停撞。另外瓷蛙,對(duì)于那些DOM的變動(dòng)(尤其是涉及頁面重新渲染的部分),通常不會(huì)立即執(zhí)行戈毒,而是每16毫秒執(zhí)行一次艰猬。這時(shí)使用requestAnimationFrame()
的效果要好于setTimeout()
。
需要注意的是埋市,setTimeout()
只是將事件插入了"任務(wù)隊(duì)列"冠桃,必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)道宅。要是當(dāng)前代碼耗時(shí)很長(zhǎng)食听,有可能要等很久胸蛛,所以并沒有辦法保證,回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行樱报。
六葬项、Node.js的Event Loop
Node.js也是單線程的Event Loop
,但是它的運(yùn)行機(jī)制不同于瀏覽器環(huán)境肃弟。
根據(jù)node
官方文檔的描述玷室,node
中的Event Loop
主要有如下幾個(gè)階段
各個(gè)階段執(zhí)行的任務(wù)如下:
- timers 階段: 這個(gè)階段執(zhí)行
setTimeout
和setInterval
預(yù)定的callback
; - I/O callbacks 階段: 執(zhí)行除了
close
事件的callbacks
零蓉、被timers
設(shè)定的callbacks
笤受、setImmediate()
設(shè)定的callbacks
這些之外的callbacks
; - idle, prepare 階段: 僅node內(nèi)部使用;
- poll 階段: 獲取新的I/O事件, 適當(dāng)?shù)臈l件下
node
將阻塞在這里; - check 階段: 執(zhí)行
setImmediate()
設(shè)定的callbacks
; - close callbacks 階段: 執(zhí)行
socket.on('close', ...)
這些 callback
process.nextTick()
process.nextTick()不屬于上面的任何一個(gè)phase,它在每個(gè)phase結(jié)束的時(shí)候都會(huì)運(yùn)行敌蜂。也可以認(rèn)為箩兽,nextTick在下一個(gè)異步方法的事件回調(diào)函數(shù)調(diào)用前執(zhí)行。
setTimeout(fn,0) Vs setImmediate Vs process.nextTick()
setTimeout(fn,0) Vs setImmediate
- setTimeout(fn,0)在timer階段執(zhí)行章喉,并且是在poll階段進(jìn)行判斷是否達(dá)到指定的time時(shí)間汗贫,若到了,就返回timer階段執(zhí)行秸脱。
- setImmediate在check階段才會(huì)執(zhí)行
有時(shí)候發(fā)現(xiàn)setImmediate先于setTimeout執(zhí)行落包,此時(shí)要分析清楚,setTimeout可能進(jìn)入了下一次的事件循環(huán)摊唇。上述的2個(gè)規(guī)則咐蝇,是建立在同一個(gè)事件循環(huán)中討論的。
例子
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('../m.txt', callback);
fs.readFile('../m.txt', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 200) {
// do nothing
}
console.log('文件回調(diào)函數(shù)完成')
});
setImmediate(() => {
console.log('immediate');
});
//輸出
immediate
文件回調(diào)函數(shù)完成
215ms have passed since I was scheduled
文件回調(diào)函數(shù)完成
上栗代碼巷查,第一次事件循環(huán)有序,timer階段定時(shí)未到、poll階段讀文件未完成然后進(jìn)入check階段岛请;第二次事件循環(huán)旭寿,timer階段沒有定時(shí)未到,poll階段崇败,io回調(diào)隊(duì)列不空盅称,執(zhí)行回調(diào),此時(shí)定時(shí)終于到了后室,返回到timer階段執(zhí)行定時(shí)器的回調(diào)函數(shù)缩膝,執(zhí)行完成又進(jìn)入poll階段
*個(gè)人總結(jié):setTimeout(fn,t)的回調(diào)函數(shù)不在check階段執(zhí)行咧擂,即便是定時(shí)時(shí)間已到逞盆。它在timer回調(diào)
setTimeout(fn,0)
&& setImmediate
兩者的執(zhí)行順序要根據(jù)當(dāng)前的執(zhí)行環(huán)境才能確定,根據(jù)官方文檔總結(jié)得出的結(jié)論是:
- 如果兩者都在主模塊(main module)調(diào)用松申,那么執(zhí)行先后取決于進(jìn)程性能云芦,即隨機(jī)俯逾。
- 如果兩者都不在主模塊調(diào)用(即在一個(gè) IO circle 中調(diào)用),那么setImmediate的回調(diào)永遠(yuǎn)先執(zhí)行舅逸。
例子1:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
//輸出:
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
栗子2:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
//輸出
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
setImmediate Vs process.nextTick()
setImmediate()屬于check觀察者桌肴,其設(shè)置的回調(diào)函數(shù),會(huì)插入到下次事件循環(huán)的末尾琉历,每次事件循環(huán)只執(zhí)行鏈表中的一個(gè)回調(diào)函數(shù)坠七。
process.nextTick()所設(shè)置的回調(diào)函數(shù)會(huì)存放到數(shù)組中,一次性執(zhí)行所有回調(diào)函數(shù)(屬于微任務(wù))旗笔。
process.nextTick()調(diào)用深度的限制彪置,上限是1000,而setImmediate沒有蝇恶;
栗子:
setImmediate(() => console.log('immediate1'));
setImmediate(() => console.log('immediate2'));
setTimeout(() => console.log('setTimeout1'), 1000);
setTimeout(() => {
console.log('setTimeout2');
process.nextTick(() => console.log('nextTick1'));
}, 0);
setTimeout(() => console.log('setTimeout3'), 0);
process.nextTick(() => console.log('nextTick2'));
process.nextTick(() => {
process.nextTick(console.log.bind(console, 'nextTick3'));
});
process.nextTick(() => console.log('nextTick4'));
//輸出
nextTick2
nextTick4
nextTick3
setTimeout2
setTimeout3
nextTick1
immediate1
immediate2
setTimeout1
分析如下:
在node中拳魁,nextTick的優(yōu)先級(jí)高于setTimeout和setImmediate(),所以會(huì)先執(zhí)行nextTick里面的信息打印撮弧。
但是對(duì)于嵌套的nextTick潘懊,會(huì)慢于同步的nextTick,所以nextTick4會(huì)先于nextTick3
然后開始一個(gè)Event Loop過程,首先執(zhí)行timer階段贿衍,而此時(shí)setTimeout所需要等待的時(shí)間是0授舟,所以立即執(zhí)行setTimeout2和setTimeout3里面的邏輯。而setTimeout1由于設(shè)置了執(zhí)行時(shí)間贸辈,不滿足執(zhí)行條件释树,被放到下一輪Event Loop
當(dāng)前Event Loop執(zhí)行到check階段,于是打印出immediate1裙椭、immediate2
執(zhí)行后面的Event Loop,當(dāng)setTimeout1達(dá)到執(zhí)行條件時(shí)執(zhí)行
node.js中的事件完成后躏哩,通知js主線程調(diào)用回調(diào)函數(shù),等到j(luò)s主線程空閑(主線程的代碼執(zhí)行完)時(shí)才去調(diào)用回調(diào)函數(shù)揉燃。
function heavyCompute(n) {
var count = 0,
i, j;
for (i = n; i > 0; --i) {
for (j = n; j > 0; --j) {
count += 1;
}
}
console.log('jisuan')
}
var t = new Date();
setTimeout(function () {
console.log(new Date() - t);
}, 1000);
fs.readFile('./a.txt',(err,data)=>{
console.log(data)
})
heavyCompute(50000);
console.log(9)
console.log(3)
//輸出
jisuan
9
3
2365
<Buffer 62 61 72 20 e5 be 88 e5 bf ab e5 b0 b1 e6 b0 b4 e7 94 b5 e8 b4 b9 62 61 72>
參考資料:
阮老師的博文
node中的Event模塊(上)
不要混淆nodejs和瀏覽器中的event loop
The Node.js Event Loop, Timers, and `process.nextTick()
前端基礎(chǔ)進(jìn)階(十二):深入核心扫尺,詳解事件循環(huán)機(jī)制