original artical link : http://www.ruanyifeng.com/blog/2014/10/event-loop.html
一赐稽、JavaScript是單線程
- JavaScript語言的一大特點(diǎn)就是單線程叫榕,也就是說,同一個(gè)時(shí)間只能做一件事姊舵。
- 為了利用多核CPU的計(jì)算能力晰绎,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程括丁,但是子線程完全受主線程控制荞下,且不得操作DOM。所以躏将,這個(gè)新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)锄弱。
二、任務(wù)隊(duì)列(task/event queue)
- 同步任務(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í)行拦赠。
具體來說巍沙,異步執(zhí)行的運(yùn)行機(jī)制如下。(同步執(zhí)行也是如此荷鼠,因?yàn)樗梢员灰暈闆]有異步任務(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)的異步任務(wù),于是結(jié)束等待狀態(tài)厦滤,進(jìn)入執(zhí)行棧援岩,開始執(zhí)行。
(4)主線程不斷重復(fù)上面的第三步掏导。
三窄俏、事件和回調(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)入主線程爵川。但是,由于存在后文提到的"定時(shí)器"功能息楔,主線程首先要檢查一下執(zhí)行時(shí)間寝贡,某些事件只有到了規(guī)定的時(shí)間,才能返回主線程钞螟。
四兔甘、Event Loop
Event Loop(事件循環(huán))
主線程從"任務(wù)隊(duì)列"中讀取事件,這個(gè)過程是循環(huán)不斷的鳞滨,所以整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán))洞焙。
主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack)拯啦,棧中的代碼調(diào)用各種外部API澡匪,它們?cè)?任務(wù)隊(duì)列"中加入各種事件(click,load褒链,done)唁情。
執(zhí)行棧中的代碼(同步任務(wù)),總是在讀取"任務(wù)隊(duì)列"(異步任務(wù))之前執(zhí)行甫匹。
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
上面代碼中的req.send方法是Ajax操作向服務(wù)器發(fā)送數(shù)據(jù)甸鸟,它是一個(gè)異步任務(wù)惦费,意味著只有當(dāng)前腳本的所有代碼執(zhí)行完,系統(tǒng)才會(huì)去讀取"任務(wù)隊(duì)列"抢韭。所以薪贫,它與下面的寫法等價(jià)。
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};
只要棧中的代碼執(zhí)行完畢刻恭,主線程就會(huì)去讀取"任務(wù)隊(duì)列"瞧省,依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
五鳍贾、定時(shí)器
"定時(shí)器"(timer)功能
除了放置異步任務(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(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í)很長,有可能要等很久仇哆,所以并沒有辦法保證辽剧,回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行。
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
setTimeout(function(){console.log(1);}, 0);
console.log(2);
六税产、Node.js的Event Loop
Node.js也是單線程的Event Loop,但是它的運(yùn)行機(jī)制不同于瀏覽器環(huán)境偷崩。
根據(jù)上圖辟拷,Node.js的運(yùn)行機(jī)制如下。
(1)V8引擎解析JavaScript腳本阐斜。
(2)解析后的代碼衫冻,調(diào)用Node API。
(3)libuv庫負(fù)責(zé)Node API的執(zhí)行谒出。它將不同的任務(wù)分配給不同的線程隅俘,形成一個(gè)Event Loop(事件循環(huán)),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給V8引擎笤喳。
(4)V8引擎再將結(jié)果返回給用戶为居。
process.nextTick方法和setImmediate方法
- process.nextTick方法
可以在當(dāng)前"執(zhí)行棧"的尾部----下一次Event Loop(主線程讀取"任務(wù)隊(duì)列")之前----觸發(fā)回調(diào)函數(shù)。也就是說杀狡,它指定的任務(wù)總是發(fā)生在所有異步任務(wù)之前蒙畴。setImmediate方法則是在當(dāng)前"任務(wù)隊(duì)列"的尾部添加事件,也就是說呜象,它指定的任務(wù)總是在下一次Event Loop時(shí)執(zhí)行膳凝,這與setTimeout(fn, 0)很像
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
- setImmediate方法
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
setImmediate指定的回調(diào)函數(shù),總是排在setTimeout前面恭陡。實(shí)際上蹬音,這種情況只發(fā)生在遞歸調(diào)用的時(shí)候
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2
process.nextTick和setImmediate的一個(gè)重要區(qū)別
多個(gè)process.nextTick語句總是在當(dāng)前"執(zhí)行棧"一次執(zhí)行完,多個(gè)setImmediate可能則需要多次loop才能執(zhí)行完休玩。