JavaScript的Event Loop詳解

前言

最近在跟團隊內(nèi)的小伙伴們一起學習和研究Vue.js的源碼,其中有一塊是nextTick函數(shù)的實現(xiàn)篷角,這個函數(shù)的主要作用就是使用宏任務或微任務來異步執(zhí)行界面的渲染更新操作等等系任,所以我本來是打算深入研究一下JavaScript的宏任務和微任務的,但是后來我發(fā)現(xiàn)我連JavaScript基本的運行機制都沒太搞懂嘉蕾。

  • 什么是調(diào)用棧(Call Stack)霜旧?
  • 什么是執(zhí)行上下文(Execution Context)?
  • JavaScript的異步任務(setTimeout以清?Promise棱貌?)又是如何運作的婚脱?
  • JavaScript的Event Loop又是個神馬東東勺像?
  • ……

帶著這些疑問吟宦,我開始了長達一個多月的探索之旅涩维。幸運的是,我找到了一些非常棒的學習資料蜗侈,它們讓我受益匪淺睡蟋,也解答了我的很大一部分問題,這就是我今天要分享給大家的我的學習成果该面,我會在這一篇博客里把我看過的這些學習資料中所講到的重點知識全部都包含進來信卡,以幫助大家更快更全面地理解JavaScript基本的運行機制。

話不多說猾瘸,我們進入正文桥嗤。

內(nèi)存的劃分

在一個經(jīng)典的計算機系統(tǒng)架構中仔蝌,程序在運行時會把分配到的內(nèi)存劃分成四個區(qū)塊,分別是:Code區(qū)塊渊鞋、Static/Global區(qū)塊瞧挤、Stack區(qū)塊以及Heap區(qū)塊。

程序運行時的內(nèi)存劃分
  • Code區(qū)塊:用于裝載程序運行的指令执俩,其實就是你編寫的代碼最終編譯成的機器指令癌刽;

  • Static/Global區(qū)塊(以下簡稱:Static區(qū)塊):用于存放全局變量尝丐。定義在函數(shù)內(nèi)的變量只能在該函數(shù)內(nèi)可見爹袁,在函數(shù)外是無法直接訪問到的矮固,但是定義在這里的變量可以在任何函數(shù)中都能夠訪問得到档址;

  • Stack區(qū)塊:即Call Stack,用于存放函數(shù)運行時的數(shù)據(jù)信息蛤迎。包括:函數(shù)調(diào)用時的參數(shù)含友、函數(shù)內(nèi)定義的變量、函數(shù)運行結束后返回的地址等等辆童;

  • Heap區(qū)塊:函數(shù)運行時的基本數(shù)據(jù)類型的數(shù)據(jù)會直接保存在Stack中惠赫,而對象類型的數(shù)據(jù)則會在Heap區(qū)塊中分配內(nèi)存進行存儲,然后返回分配內(nèi)存的起始地址以保存在Stack中聲明的變量中以便后續(xù)訪問庭砍。

Stack(調(diào)用棧)

我們目前只需要關注Stack區(qū)塊即可混埠。Stack是一個典型的棧類型數(shù)據(jù)結構(FILO:First In Last Out)。當JavaScript中的函數(shù)運行時揭北,會往Stack棧中Push一段數(shù)據(jù)吏颖,這段數(shù)據(jù)我們稱之為Stack Frame,當函數(shù)運行結束后疚俱,會將該函數(shù)對應的Stack Frame數(shù)據(jù)段Pop出棧缩多。所以夯尽,函數(shù)間的嵌套調(diào)用就會在Stack棧中堆疊一摞的Stack Frame數(shù)據(jù)段匙握。為了讓你有一個更清晰直觀的認識陈轿,接下來我們來看一段代碼(示例一):

function foo() {
  console.log('foo');
}

function bar() {
  foo();
  console.log('bar');
}

function baz() {
  bar();
  console.log('baz');
}

baz();

這段代碼很簡單,它的運行結果就是依次打印出:foo蛾娶、bar和baz潜秋。我們來看一下這段代碼在運行過程中Stack區(qū)塊的變化情況。

第0步:程序準備執(zhí)行罗售,分配并劃分內(nèi)存空間钩述,將代碼指令裝載進Code區(qū)塊并開始執(zhí)行。假設此時代碼塊的執(zhí)行函數(shù)名為main职恳,那么JavaScript Runtime會先將Stack Frame(main)壓入Stack棧中方面,然后開始調(diào)用baz函數(shù)。

第0步:程序準備執(zhí)行

第1步:調(diào)用baz函數(shù)操禀,將Stack Frame(baz)壓入Stack棧中床蜘。


第1步:調(diào)用baz函數(shù)

第2步:baz調(diào)用bar函數(shù)蔑水。將Stack Frame(bar)壓入Stack棧中搀别。


第2步:baz調(diào)用bar函數(shù)

第3步:bar調(diào)用foo函數(shù)尾抑。將Stack Frame(foo)壓入Stack棧中蒂培。


第3步:bar調(diào)用foo函數(shù)

第4步:foo調(diào)用console.log函數(shù)护戳。將Stack Frame(log)壓入Stack棧中垂睬。


第4步:foo調(diào)用console.log函數(shù)

第5步:console.log函數(shù)在控制臺打印出‘foo’驹饺,執(zhí)行完畢后將Stack Frame(log)推出Stack棧。


console.log函數(shù)執(zhí)行完畢

第6步:foo函數(shù)執(zhí)行完畢鱼炒,將Stack Frame(foo)推出Stack棧蝌借。


第6步:foo函數(shù)執(zhí)行完畢

第7步:bar調(diào)用console.log函數(shù)。將Stack Frame(log)壓入Stack棧中硬爆。


第7步:bar調(diào)用console.log函數(shù)

第8步:console.log函數(shù)在控制臺打印出‘bar’缀磕,執(zhí)行完畢后將Stack Frame(log)推出Stack棧。


第8步:console.log函數(shù)執(zhí)行完畢

第9步:bar函數(shù)執(zhí)行完畢袜蚕,將Stack Frame(bar)推出Stack棧牲剃。


第9步:bar函數(shù)執(zhí)行完畢

第10步:baz調(diào)用console.log函數(shù)雄可。將Stack Frame(log)壓入Stack棧中。


第10步:baz調(diào)用console.log函數(shù)

第11步:console.log函數(shù)在控制臺打印出‘baz’聪舒,執(zhí)行完畢后將Stack Frame(log)推出Stack棧虐急。


第11步:console.log函數(shù)執(zhí)行完畢

第12步:baz函數(shù)執(zhí)行完畢,將Stack Frame(baz)推出Stack棧被辑。


第12步:baz函數(shù)執(zhí)行完畢

第13步:程序運行結束盼理,將Stack Frame(main)推出Stack棧,Code區(qū)塊和Stack區(qū)塊均使用完畢等待被GC回收宏怔。


第13步:程序運行結束

看到這里举哟,你應該已經(jīng)對JavaScript的Call Stack有了一個更清晰直觀的認識了。

接下來妨猩,我們來聊一聊JavaScript中的“報錯”。相信大家在瀏覽器中開發(fā)時都碰到過報錯的情況威兜,這時候瀏覽器終端會輸出一段報錯信息庐椒,里面包含了錯誤發(fā)生時的Stack棧中的函數(shù)調(diào)用鏈路情況。例如笔宿,我把上面的代碼改成這樣(示例二):

function foo() {
  throw new Error('error from foo');
}

function bar() {
  foo();
}

function baz() {
  bar();
}

baz();

代碼執(zhí)行后棱诱,會在瀏覽器終端打印出下面這樣的報錯信息。

示例二:error from foo

基于Stack這樣的設計炬灭,編譯器就能夠很輕松地定位發(fā)生錯誤時的函數(shù)調(diào)用鏈路情況靡菇,我們也就能夠很方便地排查發(fā)生錯誤的原因了。

Stack Overflow(棧溢出)

很多人也碰到過棧溢出(Stack Overflow)的問題鼻吮。那么為什么會有棧溢出的情況發(fā)生呢泳唠?因為Stack棧的大小是在程序運行開始前就已經(jīng)確定下來不可變更的,所以當你往棧中存放的數(shù)據(jù)超出棧的最大容量時拓哺,就會發(fā)生棧溢出的情況脖母。通常的原因都是因為代碼的Bug導致函數(shù)無限循環(huán)嵌套調(diào)用,如同下面這個示例(示例三)所示:

示例三:棧溢出的報錯

單線程的JavaScript

我們都知道烤礁,JavaScript是一門單線程(single-threaded)的語言肥照,單線程就意味著“JavaScript Runtime只有一個Call Stack”,也意味著“JavaScript Runtime同一時間只能做一件事情”舆绎,來看看下面這段代碼(示例四):

let arr = [0, 1, 2, 3, 4, 5];

/* 平方值 */
function square(arr) {
  return arr.map((item) => item * item);
}

let res1 = square(arr);
console.log(res1); // [0, 1, 4, 9, 16, 25]

/* 立方值 */
function cube(arr) {
  return arr.map((item) => item * item * item);
}

let res2 = cube(arr);
console.log(res2); // [0, 1, 8, 27, 64, 125]

這段代碼很簡單吕朵,給定一個arr數(shù)組,分別計算輸出數(shù)組中每一個數(shù)值求平方和求立方之后的結果數(shù)組努溃。這段代碼在JavaScript中必然是順序執(zhí)行的,先求平方再求立方沦疾,但是我們不妨設想一下第队,因為square和cube函數(shù)做的事情互不相干,那么我們能不能讓它們并行執(zhí)行以提高運行效率呢彻桃?在這里因為arr數(shù)組很短晾蜘,兩個函數(shù)的計算邏輯也很簡單,所以這段代碼運行起來非常地快肆饶,但是如果arr數(shù)組非常地大,square和cube方法又進行了一些非常耗時的復雜計算的話驯镊,那么我們的設想就變得非常地有意義了。但是橄镜,可行嗎冯乘?答案是:No。之前我說過姊氓,JavaScript Runtime是單線程的喷好,它同一時間只能做一件事情。所以我們寫的JavaScript代碼只能單向串行執(zhí)行梗搅,無法并行執(zhí)行(這里暫不考慮Web Workers等技術)。

但是蟀俊,如果是這樣的話订雾,那么我們在代碼中使用setTimeout函數(shù)時,就必須等待setTimeout指定的延遲時長過后執(zhí)行回調(diào)函數(shù)烫映,然后才能繼續(xù)執(zhí)行后面的代碼噩峦,使用ajax發(fā)送請求也是同樣的情況,我們必須等到請求結果返回后執(zhí)行回調(diào)函數(shù)族淮,代碼才能繼續(xù)往后走。但是我們知道這些都不是真實的情況祝辣,那么為什么會存在這樣的矛盾點呢切油?

先不著急揭曉答案,我們先來研究一下setTimeout函數(shù)孕荠。

setTimeout

setTimeout函數(shù)基本的功能,就是接收一個回調(diào)函數(shù)和一個delay延遲時長(默認為0)稚伍,然后在delay時長過后執(zhí)行回調(diào)函數(shù)。來看一下下面的這段代碼和它的運行結果(示例五):

function foo () {
  console.log('one');

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

  console.log('three');
}

foo();
示例五

也許有些同學會對運行結果感到很意外熙涤,'three'竟然在'two'之前被打印出來困檩,我們都知道setTimeout可以延遲執(zhí)行一段函數(shù)那槽,但是為什么延遲時長設置為0都不能讓inner函數(shù)立即被執(zhí)行呢?為了探究這個問題骚灸,我們來看一下這段代碼在運行過程中Stack區(qū)塊的變化情況:

第1步:調(diào)用foo函數(shù)
第2步:foo調(diào)用console.log函數(shù)
第3步:console.log函數(shù)執(zhí)行完畢
第4步:foo調(diào)用setTimeout函數(shù)
第5步:setTimeout函數(shù)執(zhí)行完畢
第6步:foo調(diào)用console.log函數(shù)
第7步:console.log函數(shù)執(zhí)行完畢
第8步:foo函數(shù)執(zhí)行完畢
第9步:inner函數(shù)開始執(zhí)行
第10步:inner調(diào)用console.log函數(shù)
第11步:console.log函數(shù)執(zhí)行完畢
第12步:inner函數(shù)執(zhí)行完畢,程序運行結束

我們重點關注上面的第4步丈钙、第5步和第9步〗袤希可以看到星岗,當?shù)? ~ 5步調(diào)用setTimeout函數(shù)后,Stack Frame(setTimeout)莫名消失了俏橘,它接收的回調(diào)函數(shù)inner在此時并沒有被執(zhí)行,程序繼續(xù)往后走從而打印出'three'靴寂。當?shù)?步foo函數(shù)執(zhí)行完畢曹仗,也就是看似整段代碼執(zhí)行結束后,第9步inner函數(shù)又莫名出現(xiàn)在了Stack棧中并開始執(zhí)行怎茫,inner函數(shù)運行完畢后整段代碼才真正地運行結束妓灌。

我們再來看看另外一個例子(示例六):

function foo() {
  let start = Date.now();

  console.log('start');

  setTimeout(function inner() {
    console.log('inner: ' + (Date.now() - start));
  }, 2000);

  while((Date.now() - start) < 1500);
  
  console.log('end: ' + (Date.now() - start));
}

foo();
示例六

這段代碼的運行結果有兩點值得我們關注虫埂。第一點圃验,foo函數(shù)因為包含了一行空while語句而執(zhí)行了1500ms,但是setTimeout中的inner函數(shù)似乎并沒有受到任何影響澳窑,仍然在2秒鐘之后開始執(zhí)行,說明foo函數(shù)的執(zhí)行和setTimeout的計時操作是在并行執(zhí)行的鸡捐。第二麻裁,inner函數(shù)打印的時間差并不是剛剛好等于2000ms,而是2002ms煎源,而且如果你運行這段代碼的話你就會發(fā)現(xiàn),你打印的結果很可能跟我不一樣歇僧,但是一定是大于等于2000ms的一個值原献。

廬山真面目

著名的v8引擎是Chrome和NodeJS背后使用的JavaScript Runtime引擎,而你在它的源碼里是搜不到setTimeout姑隅、DOM、Ajax等字樣的慕趴,因為它本身只包含了heap和stack鄙陡,其他的setTimeout、DOM趁矾、Ajax等相關的功能都是由瀏覽器基于v8引擎之上所構建和提供的WebAPIs功能。這些WebAPIs和v8引擎一樣都是用C++編寫的详拙,它們會以獨立的線程的方式提供服務,所以我們的JavaScript Runtime是單線程的沒錯饶辙,但是當我們調(diào)用這些WebAPIs時,它們就會另起一個獨立的線程來完成各自的工作弃揽,這樣我們的JavaScript代碼才有了并發(fā)的效果。

v8引擎的結構圖如下所示:


v8引擎的結構圖

而瀏覽器的全貌圖是這樣子的:


瀏覽器的全貌圖

Event Table(事件映射表)

首先介紹一下WebAPIs部分痕慢,瀏覽器會維護一個事件映射表(Event Table)冷冗,它記錄著事件與回調(diào)函數(shù)之間的映射關系。如果你想監(jiān)聽某個DOM的click事件的話,那你就必須先在該DOM上注冊click事件滨巴,然后當該DOM接收到click事件時才會有回調(diào)函數(shù)被執(zhí)行,如果某個事件沒有被綁定回調(diào)函數(shù)的話恭取,那么該事件發(fā)生時就如同石沉大海一樣什么也不會發(fā)生。Ajax也是一樣耗跛,如果不添加返回響應時的回調(diào)函數(shù)的話攒发,那么就會變成單純的發(fā)送一個HTTP請求,也不會有后續(xù)的回調(diào)函數(shù)處理響應內(nèi)容了惠猿。setTimeout自不必說,它必須要設置一個回調(diào)函數(shù)才有意義姜凄。總而言之态秧,這些事件與回調(diào)函數(shù)之間的映射關系都會被瀏覽器記錄在Event Table表里扼鞋,以便當對應事件發(fā)生時能執(zhí)行對應的回調(diào)函數(shù)空扎。

Message Queue(消息隊列)

接下來是消息隊列Message Queue(簡稱MQ)润讥,有些文章稱之為Event Queue或者Callback Queue,說的都是同一個東西撮慨。MQ是一個典型的FIFO(First In First Out)的消息隊列脆粥,新消息會被加入到隊列的尾部,消息的執(zhí)行順序與加入隊列的順序相同变隔。每一條消息都有與之綁定的一個函數(shù),當隊首的消息被處理時猖闪,消息對應的函數(shù)就會把消息當做輸入?yún)?shù)并開始執(zhí)行。剛剛Event Table中記錄的事件發(fā)生時培慌,就會往MQ隊列中加入一條消息柑爸,然后等待被執(zhí)行

Event Loop

接下來我們就要觸及到整篇文章的重點和核心了表鳍,那就是Event Loop。剛剛我們說到譬圣,消息已經(jīng)被加入到MQ隊列中,那么消息什么時候會被處理呢偎血?這時候就該Event Loop登場了盯漂。

Event Loop實際做的事情非常地簡單:它會持續(xù)不斷地檢查Call Stack是否為空,如果為空的話就檢查MQ隊列是否有待處理的消息就缆,如果有的話就從MQ隊列的隊首取出一條消息并執(zhí)行消息綁定的函數(shù),如果沒有的話就同步監(jiān)控MQ隊列是否有新的消息加入竭宰,一旦發(fā)現(xiàn)就立即取出并執(zhí)行消息綁定的函數(shù)份招。整個過程不斷重復锁摔。

知道了Event Loop的運行機制之后,之前的幾個疑問就迎刃而解了谐腰。

首先看下示例五的setTimeout神秘消失和離奇閃現(xiàn)事件涩盾。我現(xiàn)在把第4步、第5步、第8步和第9步的完整截圖發(fā)出來給大家看看:

第4步:foo調(diào)用setTimeout函數(shù)
第5步:setTimeout函數(shù)執(zhí)行完畢
第8步:foo函數(shù)執(zhí)行完畢
第9步:inner函數(shù)開始執(zhí)行

第5步中秉氧,我們調(diào)用了瀏覽器提供的setTimeout方法,隨即啟動一個單獨的線程做計時操作离福,然后往Event Table中加入一條記錄。這里由于delay參數(shù)設置為0妖爷,所以事件會被立即觸發(fā)絮识,然后往MQ隊列中加入一條消息,由于此時Call Stack還不為空次舌,所以消息會在MQ隊列中等待兽愤。第8步中,foo函數(shù)執(zhí)行完畢浅萧,Call Stack被清空,Event Loop發(fā)現(xiàn)Call Stack為空之后立即檢查MQ隊列吩案,發(fā)現(xiàn)有一條待處理的消息,于是從隊列中取出消息并開始執(zhí)行消息綁定的函數(shù)徘郭,也就是inner函數(shù),最后inner函數(shù)執(zhí)行完畢残揉,至此整個程序運行結束。大家可以在這兒看到完整的過程抱环。

再來看下示例六的兩個問題點。第一點答案已經(jīng)揭曉了江醇,我們的JavaScript Runtime和setTimeout是在兩個獨立的線程上并行執(zhí)行的。關于第二點凛驮,我相信有些同學已經(jīng)知道答案了条辟,因為添加在setTimeout中的回調(diào)函數(shù)在倒計時結束之后并不會被立即執(zhí)行(即便delay參數(shù)被設置為0),而是需要先將消息添加到MQ隊列的隊尾羽嫡,然后等待排在前面的消息全部被處理完畢后才能開始執(zhí)行,這個過程總歸要花點時間杭棵,所以通常setTimeout回調(diào)函數(shù)執(zhí)行時的實際delay時長都要大于指定的delay時長。同樣給出示例六的完整運行過程先舷。

順便提一下滓侍,瀏覽器的每一個tab(iframe標簽和Web Workers同樣如此)都擁有自己獨立的Event Loop以及一整套的Runtime運行環(huán)境,包括Call Stack撩笆、Heap、Message Queue氮兵、Render Queue(后面會提到)等等耘擂,這樣就保證了即便某一個tab因為執(zhí)行了某種耗時的操作被阻塞,其他的tab也能夠正常運作,而不會說直接導致整個瀏覽器被卡死秩霍。不同Runtime之間的通訊方式可以看這里

Blocking(阻塞)

JavaScript號稱是一門“single-threaded(單線程)鸽照、non-blocking(非阻塞)颠悬、asynchronous(異步的)、concurrent(并發(fā)的)”編程語言赔癌。這確實是事實但也不盡然。說它是事實是因為瀏覽器將網(wǎng)絡請求灾票、文件操作(NodeJS)等幾乎所有耗時的操作都以獨立線程(concurrent)和異步回調(diào)(asynchronous)的形式提供給我們使用,所以我們的JavaScript Runtime主線程可以持續(xù)高效不間斷地執(zhí)行我們的JS代碼既们,這就是非阻塞(non-blocking)的含義正什。單線程(single-threaded)的JavaScript Runtime是優(yōu)勢也是劣勢。優(yōu)勢在于它簡化了我們編寫代碼的方式婴氮,使得我們可以不用考慮復雜的并發(fā)問題。劣勢在于一旦有耗時的操作占據(jù)了JavaScript Runtime主線程的話主经,就會導致MQ隊列中的消息無法得到及時的處理,還會阻塞UI渲染線程的執(zhí)行旨怠,進而影響到頁面的流暢性鉴腻。

我們將上面的示例六稍作改動百揭,這次我們把setTimeout的delay參數(shù)設置為500ms,來看看會發(fā)生些什么(示例七):

function foo() {
  let start = Date.now();

  console.log('start');

  setTimeout(function inner() {
    console.log('inner: ' + (Date.now() - start));
  }, 500);

  while((Date.now() - start) < 1500);
  
  console.log('end: ' + (Date.now() - start));
}

foo();
示例七

可以看到课锌,整體代碼的運行耗時依然是1500ms不變,但是我們發(fā)現(xiàn)inner函數(shù)執(zhí)行時時間也過去了1500ms渺贤,而并沒有像我們期望的那樣在500ms后就執(zhí)行,原因就是因為while((Date.now() - start) < 1500);是一句同步的操作瞭亮,它的執(zhí)行會占據(jù)JavaScript Runtime主線程和Call Stack調(diào)用棧,進而導致即便inner函數(shù)對應的消息在500ms之后就已經(jīng)在MQ隊列中等待统翩,但是由于此時Call Stack并不為空此洲,所以inner函數(shù)就無法被Event Loop及時Pick進入Call Stack執(zhí)行,它不得不等到1500ms過后Call Stack被清空呜师,然后才能被執(zhí)行。實際的運行效果請大家自行查看趟紊。

Rendering(渲染)

剛剛我們有提到碰酝,如果JavaScript Runtime主線程被阻塞的話,同樣會影響到UI渲染線程的執(zhí)行送爸,而一旦UI渲染線程被阻塞,用戶就無法在頁面上執(zhí)行點擊袭厂、滑動等操作了。這究竟是為什么呢帖烘?

原來橄杨,在瀏覽器的實現(xiàn)中,UI渲染操作(或者說是DOM更新操作)同樣是以隊列的形式處理的式矫。類似于Message Queue,瀏覽器會維護一個Render Queue(簡稱RQ)來專門存放UI渲染消息聪廉,而且它跟MQ一樣,必須等到Call Stack為空時才能被處理板熊,不同的是,它的處理優(yōu)先級是要高于MQ的竣况。界面刷新的頻次一般是每秒鐘60次,也就是每16.67ms會執(zhí)行一次丹泉,所以Event Loop每隔16.67ms就查看一下RQ隊列是否有待處理的消息鸭蛙,如果有的話就檢查Call Stack是否為空,為空就從RQ隊列取出消息并處理娶视,否則就繼續(xù)等待直至Call Stack被清空,然后再處理RQ隊列中的UI渲染消息肪获。

我相信大家都碰到過頁面卡頓的情況,原因就在這里了较木。我之前發(fā)的鏈接工具叫做loupe青柄,是一個專門用來觀察JavaScript Runtime的工具網(wǎng)站,打開它并點擊左上角的圖標就可以展開設置面板致开,里面可以設置代碼運行時停頓的時長,還可以模擬UI渲染操作虹蒋,勾中之后就可以查看當主線程代碼運行時,UI渲染消息被阻塞的過程了千诬。我們還是以示例六為例膏斤,來看看實際的運行效果:

RQ隊列被阻塞

再談Blocking

我們已經(jīng)知道莫辨,JavaScript Runtime主線程的阻塞會導致RQ隊列和MQ隊列中的消息無法被及時處理,所以我們要盡量避免執(zhí)行一些同步耗時的操作沮榜,要給到這些隊列中的消息被處理的機會。

同樣草巡,會阻礙隊列消息被及時處理的還有隊列本身被阻塞的情況型酥。比較典型的場景是在document的onscroll事件上綁定了回調(diào)函數(shù),由于onscroll事件觸發(fā)的頻次同樣是每秒60次弥喉,所以當用戶滾動頁面時,很容易就會把MQ隊列塞滿棚亩,如果回調(diào)函數(shù)里還執(zhí)行了一些UI渲染等耗時的操作的話虏杰,那簡直就是災難性的,畢竟UI渲染線程和JavaScript Runtime主線程是無法并行執(zhí)行的(運行效果傳送門)纺阔。

尾聲

至此我的分享就結束了,感謝Philip Roberts在2014年歐洲JSConf上精彩的演講州弟,是他讓我真正搞明白了JavaScript的Event Loop究竟是如何工作的钧栖,之前提到的loupe也是他的杰作,附上油管鏈接優(yōu)酷鏈接以供各位看官享用:)

參考資料

  1. What the heck is the event loop anyway? | Philip Roberts | JSConf EU
  2. Concurrency model and Event Loop
  3. The JavaScript Event Loop
  4. Understanding JS: The Event Loop
  5. JavaScript Event Loop Explained

***更新@2019年07月07日***

閉包

最近又看了一篇博客(The JavaScript Event Loop: Explained)婆翔,引發(fā)了我對于閉包的思考拯杠,考慮如下代碼:

function foo() {
  let a = {
    name: 'Chris, Z',
    gender: 'Man',
  };
  
  let b = 'Baby';
  
  let c = 1024;
  
  let d = true;
  
  setTimeout(function inner() {
    console.log(a);
    console.log(b);
    console.log(c);
    console.log(d);
  });
}

foo();

按照我們之前所說的,當foo執(zhí)行完畢后它對應的stack frame(foo)就被移出Call Stack棧而不復存在了啃奴,但是我們也知道inner函數(shù)執(zhí)行時是能夠訪問到foo函數(shù)內(nèi)定義的abcd變量的潭陪,這不是矛盾了嗎?
我其實也沒找到具體的資料解釋這一塊Runtime引擎是怎么處理的最蕾,所以我大膽地設想了幾種可能的做法:

做法一

stack frame(foo)出棧時確實被內(nèi)存回收了,但是Runtime引擎在這里做了優(yōu)化黎炉,inner函數(shù)會將abcd變量的值拷貝下來保存到某個地方慷嗜,由于a變量指向了堆中的一個對象,b變量指向了堆中的一個字符串常量庆械,它們都是引用值,所以當inner函數(shù)將ab變量的引用地址值保存下來時沐序,stack frame(foo)中聲明的ab變量本身就可以被放心地回收了堕绩,ab變量所指向的堆地址由于仍然被inner函數(shù)所引用而不會被GC回收,進而可以在inner函數(shù)執(zhí)行時被引用到垄惧。而cd變量就更簡單了绰寞,它們只是原始類型而已,直接被inner函數(shù)拷貝保存下來就可以了觉壶,既不會影響stack frame(foo)的內(nèi)存回收件缸,也不會影響inner函數(shù)執(zhí)行時引用到cd變量的值。

做法二

stack frame(foo)并不會真正出棧(邏輯上已經(jīng)出棧争剿,但物理上仍然占據(jù)棧內(nèi)存)蚕苇,inner函數(shù)也無需在執(zhí)行前保存它引用的變量值凿叠。那么此時Call Stack在內(nèi)存空間上就會形成“空洞”,只不過Runtime引擎會很好地處理這種情況蹬碧,不會讓后續(xù)的stack frame入棧和出棧感受到“空洞”的存在而已炒刁。

做法三

前面的跟做法二一樣,只不過Call Stack會直接用跳過stack frame(foo)的一個新地址作為起始地址開始構建飒筑,這樣就不會形成“空洞”了。

當然,上面的這些都只是我個人的猜想而已全谤,如果誰有確切的答案還望不吝賜教。

參考資料

  1. JavaScript main thread. Dissected.
  2. The JavaScript Event Loop: Explained
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末补憾,一起剝皮案震驚了整個濱河市盈匾,隨后出現(xiàn)的幾起案子毕骡,更是在濱河造成了極大的恐慌,老刑警劉巖窿撬,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叙凡,死亡現(xiàn)場離奇詭異握爷,居然都是意外死亡,警方通過查閱死者的電腦和手機追城,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門师抄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辆布,你說我怎么就攤上這事茶鉴。” “怎么了惭蹂?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長媚污。 經(jīng)常有香客問我廷雅,道長,這世上最難降的妖魔是什么商架? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任芥玉,我火速辦了婚禮,結果婚禮上赶袄,老公的妹妹穿的比我還像新娘弃鸦。我一直安慰自己幢痘,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布购岗。 她就那樣靜靜地躺著门粪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪乾吻。 梳的紋絲不亂的頭發(fā)上拟蜻,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天酝锅,我揣著相機與錄音,去河邊找鬼爸舒。 笑死,一個胖子當著我的面吹牛扭勉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嫉入,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼熬拒!你這毒婦竟也來了?” 一聲冷哼從身側響起蛀序,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤徐裸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后重贺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體气笙,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡怯晕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年舟茶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隧出。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡客燕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赏廓,到底是詐尸還是另有隱情,我是刑警寧澤摸柄,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布既忆,位于F島的核電站,受9級特大地震影響跃脊,放射性物質發(fā)生泄漏苛吱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一绘雁、第九天 我趴在偏房一處隱蔽的房頂上張望庐舟。 院中可真熱鬧住拭,春花似錦、人聲如沸废酷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽睹簇。三九已至寥闪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凿渊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工搪锣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留构舟,地道東北人堵幽。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像努咐,于是被迫代替她去往敵國和親殴胧。 傳聞我的和親對象是個殘疾皇子溃肪,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容