??【異步】1. 事件循環(huán) & ES6 任務隊列

事件循環(huán)與任務隊列是JS中比較重要的兩個概念构韵。這兩個概念在ES5和ES6兩個標準中有不同的實現(xiàn)泉沾。尤其在ES6標準中,清楚的區(qū)分宏觀任務隊列和微觀任務隊列才能解釋Promise一些看似奇怪的表現(xiàn)。

JS引擎本身所做的只不過是在需要的時候垦页,在給定的任意時刻執(zhí)行程序中的單個代碼塊掌唾。JS引擎并不是獨立運行的放前,它運行在宿主環(huán)境:Web瀏覽器忿磅、Node.js、機器人等各種設備中凭语。所有這些環(huán)境都有一個共同點(thread葱她,線程),即它們都提供了一種機制來處理程序中多個塊的執(zhí)行似扔,且執(zhí)行每塊時調用JavaScript引擎吨些,這種機制被稱為事件循環(huán)。
所以說炒辉,JavaScript引擎本身并沒有時間的概念豪墅,只是一個按需執(zhí)行JavaScript任意代碼片段的環(huán)境∏埽“事件”(JavaScript代碼執(zhí)行)調度總是由包含它的環(huán)境進行偶器。

直到ES6,JavaScript才真正內(nèi)建有直接的異步概念缝裤,即ES6從本質上改變了在哪里管理事件循環(huán)屏轰,ES6精確指定了事件循環(huán)的工作細節(jié),這意味著在技術上將其納入JavaScript引擎的勢力范圍憋飞,而不是只由宿主環(huán)境來管理亭枷。
ES6中Promise的引入,這項技術要求對事件循環(huán)隊列的調度運行能夠直接進行精細控制搀崭。

事件循環(huán)

一旦有事件需要運行叨粘,事件循環(huán)就會運行,直到隊列清空瘤睹。事件循環(huán)的每一輪稱為一個tick升敲。用戶交互、IO轰传、定時器會向事件隊列中加入事件驴党。

var eventLoop = []; //用做隊列的數(shù)組,先進先出
var event;

while(true){ //永遠執(zhí)行
  if(eventLoop.length > 0){ //一次tick
    event = eventLoop.shift(); //拿到隊列中的下一個事件
    try {
      event();
    } catch (err) {
      reportError(err);
    }
  }
}

在瀏覽器的事件循環(huán)中获茬,首先大家要認清楚 3 個角色:函數(shù)調用棧港庄、宏任務(macro-task)隊列微任務(micro-task)隊列

  • 函數(shù)調用棧:當引擎第一次遇到 JS 代碼時恕曲,會產(chǎn)生一個全局執(zhí)行上下文并壓入調用棧鹏氧。后面每遇到一個函數(shù)調用,就會往棧中壓入一個新的函數(shù)上下文佩谣。JS引擎會執(zhí)行棧頂?shù)暮瘮?shù)把还,執(zhí)行完畢后,彈出對應的上下文。
  • 任務隊列:對于一些異步的任務吊履,不需要立刻被執(zhí)行安皱,屬于待執(zhí)行的任務,會按照一定規(guī)則排隊艇炎,等待被推入調用棧的時刻到來酌伊。這個隊列就是任務隊列。

Q:為什么需要 Event Loop缀踪?

javascript的一個特點就是單線程腺晾,但是很多時候我們?nèi)匀恍枰诓煌臅r間去執(zhí)行不同的任務,例如給元素添加點擊事件辜贵,設置一個定時器,或者發(fā)起Ajax請求归形。因此需要一個異步機制來達到這樣的目的托慨,事件循環(huán)機制也因此而來。

var a = 1;
var b = 2;
function foo(){
  a++;
  b = b * a;
  a = b + 3;
}
function bar(){
  b--;
  a = 8 + b;
  b = a * 2;
}

ajax("http://some.url.1", foo);
ajax("http://some.url.2", bar);

由于JavaScript的單線程特性暇榴,foo()以及bar()中的代碼具有原子性厚棵。也就是說,一旦foo()開始運行蔼紧,它的所有代碼都會在bar()中的任意代碼運行之間完成婆硬,或者相反。這稱為完整運行特性奸例。
由于foo()不會被bar()中斷彬犯,bar()也不會被foo()中斷,所以這個程序只有兩個可能的輸出查吊,取決于這兩個函數(shù)哪個先運行谐区。如果存在多線程,且foo()和bar()中的語句可以交替運行的話(并行線程逻卖,共享內(nèi)存)宋列,可能輸出的情況會增加不少。
同一段代碼有兩個可能輸出意味著存在不確定性评也!但是炼杖,這種不確定性是在函數(shù)(事件)順序級別上,而不是多線程情況下的語句順序級別盗迟。因此坤邪,這一確定性要高于多線程情況。
在JavaScript的特性中罚缕,這種函數(shù)順序的不確定性就是通常所說的競態(tài)條件,foo()和bar()相互競爭罩扇,看誰先運行。具體來說,因為無法可靠預測a和b的最終結果喂饥,所以才是競態(tài)條件消约。

??:如果JavaScript中某個函數(shù)由于某種原因不具有完整運行特性,那么可能的結果就會多得多员帮,對吧或粮?實際上,ES6就引入了這么一個東西捞高!

  • 異步:是關于現(xiàn)在和將來的時間間隙氯材;(事件循環(huán)把自身的工作分成一個個任務并順序執(zhí)行,不允許對共享內(nèi)存的并行訪問和修改)
    JavaScript程序總是至少分為兩個塊:第一塊現(xiàn)在運行硝岗;下一塊將來運行氢哮,以響應某個事件。盡管程序是一塊一塊執(zhí)行的型檀,但是所有這些塊共享對程序作用域和狀態(tài)的訪問冗尤,所以對狀態(tài)的修改都是在之前累積的修改之上進行的。
  • 并行:是關于能夠同時發(fā)生的事情胀溺;(并行計算的最常見工具就是進程和線程)并行線程
  • 并發(fā):兩個或多個“進程”同時執(zhí)行就出現(xiàn)了并發(fā)裂七,不管組成它們的單個運行是否并行執(zhí)行(在獨立的處理器或處理器核心上同時運行)。并發(fā)可以看作是“進程”級(或任務級)的并行仓坞。
    并發(fā)指兩個或多個事件鏈隨時間發(fā)展交替執(zhí)行背零,以至于從更高的層次來看,就像是同時在運行(盡管在任意時刻只處理一個事件)无埃。
    單線程事件循環(huán)是并發(fā)的一種形式徙瓶。

任務隊列

在ES6中,有一個新的概念建立在事件循環(huán)隊列之上嫉称,叫作任務隊列倍啥。這個概念給大家?guī)淼淖畲笥绊懣赡苁荘romise的異步特性。
任務隊列澎埠,是掛在事件循環(huán)隊列的每個tick之后的一個隊列虽缕。在事件循環(huán)的每個tick中,可能出現(xiàn)的異步動作不會導致一個完整的新事件添加到事件循環(huán)隊列中蒲稳,而會在當前tick的任務隊列末尾添加一個項目(一個任務)氮趋。

  • 事件循環(huán)隊列:玩過了一個游戲之后,需要重新到隊尾排隊才能再玩一次江耀;
  • 任務隊列:玩過了游戲之后剩胁,插隊接著繼續(xù)玩;

JavaScript 代碼的執(zhí)行過程中祥国,除了依靠函數(shù)調用棧來搞定函數(shù)的執(zhí)行順序外昵观,還依靠任務隊列(task queue)來搞定另外一些代碼的執(zhí)行:

  • 一個線程中晾腔,事件循環(huán)是唯一的,但任務隊列可以擁有多個啊犬;
  • 任務隊列又分為 macro-task(宏任務)與 micro-task(微任務)灼擂,在最新標準中,它們被分別稱為 task 與 jobs 觉至;
  • 來自不同源的任務會進入到不同的任務隊列剔应,其中 setTimeout 和 setInterval 是同源的,常見的任務隊列如下:
不同源的任務會進入到不同的任務隊列.png
  • 事件循環(huán)的順序语御,決定了JavaScript代碼的執(zhí)行順序峻贮。它從script(整體代碼)開始第一次循環(huán)。之后全局上下文進入函數(shù)調用棧应闯。直到調用棧清空(只剩全局)纤控,然后執(zhí)行所有的micro-task。當所有可執(zhí)行的micro-task執(zhí)行完畢之后碉纺。循環(huán)再次從macro-task開始船万,找到其中一個任務隊列執(zhí)行完畢,然后再執(zhí)行所有的micro-task惜辑,這樣一直循環(huán)下去;
  • 其中每一個任務的執(zhí)行疫赎,無論是macro-task還是micro-task盛撑,都是借助函數(shù)調用棧來完成;

宏任務 macro-task(task):

  • script(整體代碼)
  • setTimeout
  • setInterval
  • setImmediate(僅存在于Node.js環(huán)境中)
  • I/O操作
  • request / AnimationFrame
  • UI render

setTimeout 是宿主環(huán)境提供的API捧搞,其作為一個任務分發(fā)器抵卫,這個函數(shù)會立即執(zhí)行,而它所要分發(fā)但任務胎撇,也就是它的第一個參數(shù)介粘,才是延遲執(zhí)行。

setTimeout 函數(shù)的返回值是一個整數(shù)晚树,返回的是一個 ID號姻采,從1開始。clearTimeout(timeid) 會清除這個定時器爵憎,但不會改版id值慨亲。

當我們在執(zhí)行setTimeout任務中遇到setTimeout時,它仍然會將對應的任務分發(fā)到setTimeout隊列中去宝鼓,但是該任務就得等到下一輪事件循環(huán)執(zhí)行了刑棵。

微任務 micro-task(job):

  • process.nextTick(僅存在于Node11之后 是微任務的一種)
  • MutationObserver(html5新特性)
  • Object.observe(已廢棄)
  • Promise
  • Async/Await(實際上就是promise)

nextTick 隊列會比 Promise 先執(zhí)行。nextTick中的可執(zhí)行任務執(zhí)行完畢之后愚铡,才會開始執(zhí)行Promise隊列中的任務蛉签。

Node.js VS 瀏覽器環(huán)境中的事件循環(huán)機制

  1. 瀏覽器中有事件循環(huán),Node.js 中也有。事件循環(huán)是Node處理非阻塞I/O操作的機制碍舍,Node.js 中事件循環(huán)的實現(xiàn)是依靠的libuv引擎柠座。由于 Node.js 11 之后,事件循環(huán)的一些原理發(fā)生了變化乒验,因此 Node11事件循環(huán)已與瀏覽器事件循環(huán)機制趨同愚隧。
  2. chrome瀏覽器中新標準中的事件循環(huán)機制與 Node.js 類似,都有宏任務和微任務之分锻全。但是有些API只有 Node.js 中有狂塘,而瀏覽器中沒有,比如 process.nextTicksetImmediate 鳄厌。
  3. 瀏覽器中的微任務是在每個相應的宏任務中執(zhí)行的荞胡。而 Node.js 中的微任務是在不同階段之間執(zhí)行的。
  4. 瀏覽器的 Event-Loop 由各個瀏覽器自己實現(xiàn)了嚎;而 Node 的 Event-Loop 由 libuv 來實現(xiàn)泪漂。
  5. 在瀏覽器中,只有一個微任務隊列需要接受處理歪泳;在Node中萝勤,有兩類微任務隊列:next-tick隊列和其他隊列。 其中這個 next-tick 隊列呐伞,專門用來收斂 process.nextTick 派發(fā)的異步任務敌卓。在清空隊列時,優(yōu)先清空 next-tick 隊列中的任務伶氢,隨后才會清空其它微任務趟径。
  6. 在瀏覽器中,我們每次出隊并執(zhí)行一個宏任務癣防;而在 Node 中蜗巧,我們每次會嘗試清空當前階段對應宏任務隊列里的所有任務(除非達到了系統(tǒng)限制);

Node.js 技術架構

Node整體上由這三部分組成:

  1. 應用層:這一層就是大家最熟悉的 Node.js 代碼蕾盯,包括 Node 應用以及一些標準庫幕屹。
  2. 橋接層:Node 底層是用 C++ 來實現(xiàn)的。橋接層負責封裝底層依賴的 C++ 模塊的能力级遭,將其簡化為 API 向應用層提供服務香嗓。
  3. 底層依賴:這里就是最最底層的 C++ 庫了,支撐 Node 運行的最基本能力在此匯聚装畅。其中需要特別引起大家注意的就是 V8 和 libuv:
  • V8 是 JS 的運行引擎靠娱,它負責把 JavaScript 代碼轉換成 C++,然后去跑這層 C++ 代碼掠兄。
  • libuv:它對跨平臺的異步I/O能力進行封裝像云,同時也是我們本節(jié)的主角:Node 中的事件循環(huán)就是由 libuv 來初始化的锌雀。
    瀏覽器的 Event-Loop 由各個瀏覽器自己實現(xiàn);而 Node 的 Event-Loop 由 libuv 來實現(xiàn)迅诬。
    Node.js技術架構.png

libuv 中 Event-Loop 實現(xiàn)

libuv 主導循環(huán)機制共有六個循環(huán)階段:

libuv中Event-Loop.png

  • timers階段:執(zhí)行 setTimeout 和 setInterval 中定義的回調腋逆;
  • pending callbacks:直譯過來是“被掛起的回調”,如果網(wǎng)絡I/O或者文件I/O的過程中出現(xiàn)了錯誤侈贷,就會在這個階段處理錯誤的回調(比較少見惩歉,可以略過);
  • idle, prepare:僅系統(tǒng)內(nèi)部使用俏蛮。這個階段我們開發(fā)者不需要操心撑蚌。(可以略過);
  • poll (輪詢階段):重點階段搏屑,這個階段會執(zhí)行I/O回調争涌,同時還會檢查定時器是否到期;在 poll 階段處理的回調中辣恋,如果既派發(fā)了 setImmediate亮垫、又派發(fā)了 setTimeout,那么這個順序是板上釘釘?shù)摹欢ㄊ窍葓?zhí)行 setImmediate伟骨,再執(zhí)行 setTimeout饮潦。
  • check(檢查階段):處理 setImmediate 中定義的回調;
  • close callbacks:處理一些“關閉”的回調携狭,比如socket.on('close', ...)就會在這個階段被觸發(fā)继蜡。

Node11前后的變化

setTimeout(() => {
  console.log('timeout1');
}, 0);   

setTimeout(() => {
  console.log('timeout2');
  Promise.resolve().then(function() {
    console.log('promise1');
  });
}, 0);

setTimeout(() => {
  console.log('timeout3')
}, 0)

Node11開始,timers 階段的setTimeout暑中、setInterval等函數(shù)派發(fā)的任務壹瘟、包括 setImmediate 派發(fā)的任務鲫剿,都被修改為:一旦執(zhí)行完當前階段的一個任務鳄逾,就立刻執(zhí)行微任務隊列。
上述代碼輸出結果:

  • Node v9.3.0:timeout1 timeout2 timeout3 promise1 在 timers 階段灵莲,依次執(zhí)行了所有的 setTimeout 回調雕凹、清空了隊列。
  • Node v12.4.1:timeout1 timeout2 promise1 timeout3
  • 瀏覽器:timeout1 timeout2 promise1 timeout3

測試一下

例1:

console.log('script start');
async function async1(){
    await async2();
    console.log('async1 end');
}
async function async2(){
    console.log('async2 end');
}
async1();

setTimeout(function(){
    console.log('setTimeout')
}, 0);

new Promise(resolve => {
    console.log('Promise');
    resolve();
}).then(function(){
    console.log('promise1');
}).then(function(){
    console.log('promise2');
})
console.log('script end');
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout 
  • 首先政冻,事件循環(huán)從宏任務隊列開始枚抵,此時宏任務隊列中只有一個script(整體代碼)任務。所以執(zhí)行代碼明场,輸出 script start 汽摹;
  • 執(zhí)行 async1(),會調用 async2()苦锨,輸出 async2 end逼泣。此時會保留 async1 函數(shù)的上下文趴泌,將await后面的代碼注冊為一個微任務,然后跳出 async1 函數(shù)拉庶;
  • 遇到 setTimeout嗜憔,產(chǎn)生一個宏任務
  • 遇到Promise實例氏仗,Promise構造函數(shù)中的第一個參數(shù)吉捶,是在new的時候執(zhí)行,因此不會進入任何其他的隊列皆尔,而是直接在當前任務直接執(zhí)行了呐舔,輸出 Promise,后續(xù)的 .then 產(chǎn)生第二個微任務床佳,會被分發(fā)到 micro-task 的Promise隊列中滋早;
  • 繼續(xù)執(zhí)行代碼,輸出 script end砌们;
  • 代碼邏輯執(zhí)行完成(當前宏任務執(zhí)行完畢)杆麸,開始執(zhí)行當前宏任務產(chǎn)生的微任務隊列,輸出async1 end浪感,繼續(xù)執(zhí)行下一個微任務輸出 promise1昔头,該微任務遇到 then,產(chǎn)生一個新的微任務影兽;
  • 執(zhí)行產(chǎn)生的微任務揭斧,輸出 promise2,當前微任務隊列執(zhí)行完畢峻堰;
  • 執(zhí)行下一個宏任務讹开,輸出 setTimeout

例2:

console.log('script start')
async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
        console.log('async2 end1')
    })
}
async1()

setTimeout(function() {
    console.log('setTimeout')
}, 0)

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function() {
    console.log('promise1')
})
.then(function() {
    console.log('promise2')
})
console.log('script end')
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout 

此時執(zhí)行完awit并不先把await后面的代碼注冊到微任務隊列中去捐名,而是執(zhí)行完await之后旦万,直接跳出async1函數(shù),執(zhí)行其他代碼镶蹋。然后遇到promise的時候成艘,把promise.then注冊為微任務。

其他

瀏覽器&Node中的事件循環(huán)
JS事件機制可視化
理解Promise之任務隊列

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贺归,一起剝皮案震驚了整個濱河市淆两,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拂酣,老刑警劉巖秋冰,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異婶熬,居然都是意外死亡剑勾,警方通過查閱死者的電腦和手機光坝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甥材,“玉大人盯另,你說我怎么就攤上這事≈拚裕” “怎么了鸳惯?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長叠萍。 經(jīng)常有香客問我芝发,道長,這世上最難降的妖魔是什么苛谷? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任龄寞,我火速辦了婚禮张惹,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己述吸,他們只是感情好惨恭,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布牲平。 她就那樣靜靜地躺著皂贩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪自沧。 梳的紋絲不亂的頭發(fā)上坟奥,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音拇厢,去河邊找鬼爱谁。 笑死,一個胖子當著我的面吹牛孝偎,可吹牛的內(nèi)容都是我干的访敌。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼邪媳,長吁一口氣:“原來是場噩夢啊……” “哼捐顷!你這毒婦竟也來了荡陷?” 一聲冷哼從身側響起雨效,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎废赞,沒想到半個月后徽龟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡唉地,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年据悔,在試婚紗的時候發(fā)現(xiàn)自己被綠了传透。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡极颓,死狀恐怖朱盐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情菠隆,我是刑警寧澤兵琳,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站骇径,受9級特大地震影響躯肌,放射性物質發(fā)生泄漏。R本人自食惡果不足惜破衔,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一清女、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晰筛,春花似錦嫡丙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至卦方,卻和暖如春羊瘩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盼砍。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工尘吗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浇坐。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓睬捶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親近刘。 傳聞我的和親對象是個殘疾皇子擒贸,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344