JavaScript 運行機制 & EventLoop
看阮老師博客和自己的理解宴杀,記錄的學習筆記,js的單線程和 事件EventLoop 機制购桑。
1. JavaScript是單線程
JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事屏积。
js這門語言在剛開始創(chuàng)造時,就是作為瀏覽器腳本語言伸但,只要用途是與用戶互動肾请,操作dom,這決定了它只能為單線程更胖,單線程已成為JavaScript這門語言的核心特征铛铁,將來也不會改變。
HTML5提出Web Worker標準却妨,允許JavaScript腳本創(chuàng)建多個線程饵逐,但是子線程完全受主線程控制,且不得操作DOM彪标。所以倍权,這個新標準并沒有改變JavaScript單線程的本質(zhì)
2. 任務隊列
單線程就意味著,所有任務需要排隊捞烟,前一個任務結束薄声,才會執(zhí)行后一個任務。如果前一個任務耗時很長题画,后一個任務就不得不一直等著默辨,導致很多時候CPU都是空閑著的。
JavaScript語言的設計者意識到苍息,這時主線程完全可以不管IO設備缩幸,掛起處于等待中的任務,先運行排在后面的任務竞思。等到IO設備返回了結果表谊,再回過頭,把掛起的任務繼續(xù)執(zhí)行下去盖喷。
于是爆办,JavaScript任務可以分兩種,同步任務和異步任務
同步任務
同步任務指的是传蹈,在主線程上排隊執(zhí)行的任務押逼,只有前一個任務執(zhí)行完畢步藕,才能執(zhí)行后一個任務。
異步任務
異步任務指的是挑格,不進入主線程咙冗,而進入任務隊列
中的任務,只有任務隊列
通知主線程漂彤,某個異步任務可以執(zhí)行了雾消,這個任務才會進入主線程執(zhí)行。
異步執(zhí)行的具體運行機制
同步執(zhí)行也是如此挫望,同步執(zhí)行可以被視為沒有異步任務的異步執(zhí)行
- 所有同步任務都在主線程上執(zhí)行立润,形成一個執(zhí)行棧
- 主線程之外,還存在一個"任務隊列"媳板,只要異步任務有了運行結果桑腮,就在"任務隊列"之中放置一個事件。
- 一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢蛉幸,系統(tǒng)就會讀取"任務隊列"破讨,看看里面有哪些事件。那些對應的異步任務奕纫,于是結束等待狀態(tài)提陶,進入執(zhí)行棧,開始執(zhí)行匹层。
- 主線程不斷重復上面的第三步隙笆。
只要主線程空了,就會去讀取"任務隊列"升筏,這就是JavaScript的運行機制撑柔。這個過程會不斷重復。
3. 事件和回調(diào)函數(shù)
Event queue
- -- 任務隊列 是一個事件的隊列您访,IO設備完成一項任務乏冀,就會在任務隊列中添加一個事件,表明相關的異步任務可以進行“執(zhí)行椦笾唬”了,主程序讀取任務隊列昼捍,就是讀取里面的事件识虚。
Event queue
中的事件,除了IO設備的事件妒茬,還包括一些用戶的操作事件担锤,如鼠標點擊,頁面滾動等等乍钻,只要指定過回調(diào)函數(shù)肛循,這些事件都會進入任務隊列铭腕,當主線程開始執(zhí)行異步任務,就是執(zhí)行對應的回調(diào)函數(shù)多糠。
"任務隊列"是一個先進先出的數(shù)據(jù)結構累舷,排在前面的事件,優(yōu)先被主線程讀取夹孔。主線程的讀取過程基本上是自動的被盈,只要執(zhí)行棧一清空,"任務隊列"上第一位的事件就自動進入主線程搭伤。
但是只怎,由于存在"定時器"功能,主線程首先要檢查一下執(zhí)行時間怜俐,某些事件只有到了規(guī)定的時間身堡,才能返回主線程。
4. Event Loop
主線程從"任務隊列"中讀取事件拍鲤,這個過程是循環(huán)不斷的贴谎,所以整個的這種運行機制又稱為Event Loop(事件循環(huán))。
主程序運行時殿漠,產(chǎn)生堆和棧赴精,棧中的代碼調(diào)用各位外部的WEBAPI,他們在任務隊列中加入各種事件绞幌,如圖中的Click蕾哟,Load,Done莲蜘。只要棧中的代碼執(zhí)行完畢谭确,主線程就會去讀取"任務隊列",依次執(zhí)行那些事件所對應的回調(diào)函數(shù)票渠。
棧中的代碼(同步任務)總是在任務隊列(異步任務)之前執(zhí)行
5. 定時器
除了放置異步任務的事件逐哈,"任務隊列"還可以放置定時事件
定時器功能主要由setTimeout()和setInterval()這兩個函數(shù)來完成,它們的內(nèi)部運行機制完全一樣问顷,區(qū)別在于前者指定的代碼是一次性執(zhí)行昂秃,后者則為反復執(zhí)行。以下主要討論setTimeout()杜窄。
setTimeout(function(){console.log(1);}, 0);
console.log(2);
上面代碼的執(zhí)行結果總是2肠骆,1,因為只有在執(zhí)行完第二行以后塞耕,系統(tǒng)才會去執(zhí)行"任務隊列"中的回調(diào)函數(shù)蚀腿,盡管設置的推遲毫秒數(shù)為0.
HTML5標準規(guī)定了setTimeout()的第二個參數(shù)的最小值(最短間隔),不得低于4毫秒扫外,如果低于這個值莉钙,就會自動增加廓脆。
需要注意的是,setTimeout()只是將事件插入了"任務隊列"磁玉,必須等到當前代碼(執(zhí)行棧)執(zhí)行完停忿,主線程才會去執(zhí)行它指定的回調(diào)函數(shù)。要是當前代碼耗時很長蜀涨,有可能要等很久瞎嬉,所以并沒有辦法保證,回調(diào)函數(shù)一定會在setTimeout()指定的時間執(zhí)行厚柳。
6. Node.js的Event Loop
Node.js也是單線程的Event Loop氧枣,但是它的運行機制不同于瀏覽器環(huán)境。
- nodejs的event是基于libuv别垮,而瀏覽器的event loop則在html5的規(guī)范中明確定義便监。
- libuv已經(jīng)對event loop作出了實現(xiàn),而html5規(guī)范中只是定義了瀏覽器中event loop的模型碳想,具體實現(xiàn)留給了瀏覽器廠商烧董。
根據(jù)上圖,Node.js的運行機制如下胧奔。
(1)V8引擎解析JavaScript腳本逊移。
(2)解析后的代碼,調(diào)用Node API龙填。
(3)libuv庫負責Node API的執(zhí)行胳泉。它將不同的任務分配給不同的線程,形成一個Event Loop(事件循環(huán))岩遗,以異步的方式將任務的執(zhí)行結果返回給V8引擎扇商。
(4)V8引擎再將結果返回給用戶。
除了setTimeout和setInterval這兩個方法宿礁,Node.js還提供了另外兩個與"任務隊列"有關的方法:process.nextTick和setImmediate案铺。它們可以幫助我們加深對"任務隊列"的理解。
- process.nextTick方法可以在當前"執(zhí)行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")之前----觸發(fā)回調(diào)函數(shù),它指定的任務總是發(fā)生在所有異步任務之前梆靖。
- setImmediate方法則是在當前"任務隊列"的尾部添加事件控汉,也就是說,它指定的任務總是在下一次Event Loop時執(zhí)行返吻,這與setTimeout(fn, 0)很像暇番。
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('Yo~~');
}, 0)
// 1
// 2
// Yo~~
上面代碼中,由于process.nextTick方法指定的回調(diào)函數(shù)思喊,總是在當前"執(zhí)行棧"的尾部觸發(fā),所以不僅函數(shù)A比setTimeout指定的回調(diào)函數(shù)timeout先執(zhí)行次酌,而且函數(shù)B也比timeout先執(zhí)行恨课。這說明舆乔,如果有多個process.nextTick語句(不管它們是否嵌套),將全部在當前"執(zhí)行棧"執(zhí)行剂公。
現(xiàn)在希俩,再看setImmediate
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
上面代碼中,setImmediate與setTimeout(fn,0)各自添加了一個回調(diào)函數(shù)A和timeout纲辽,都是在下一次Event Loop觸發(fā)颜武。那么,哪個回調(diào)函數(shù)先執(zhí)行呢拖吼?答案是不確定鳞上。運行結果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2吊档。
令人困惑的是篙议,Node.js文檔中稱,setImmediate指定的回調(diào)函數(shù)怠硼,總是排在setTimeout前面鬼贱。實際上,這種情況只發(fā)生在遞歸調(diào)用的時候香璃。
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
上面代碼中这难,setImmediate和setTimeout被封裝在一個setImmediate里面,它的運行結果總是1--TIMEOUT FIRED--2葡秒,這時函數(shù)A一定在timeout前面觸發(fā)姻乓。至于2排在TIMEOUT FIRED的后面(即函數(shù)B在timeout后面觸發(fā)),是因為setImmediate總是將事件注冊到下一輪Event Loop同云,所以函數(shù)A和timeout是在同一輪Loop執(zhí)行糖权,而函數(shù)B在下一輪Loop執(zhí)行。
process.nextTick和setImmediate的一個重要區(qū)別:多個process.nextTick語句總是在當前"執(zhí)行棧"一次執(zhí)行完炸站,多個setImmediate可能則需要多次loop才能執(zhí)行完星澳。
js 和 node 的原理知識還是很多地方都不甚了解,加油努力學習旱易!