ar data=[
{name:"aa",age:16,gender:"male"},
{name:"bb",age:18,gender:"female"},
{name:"cc",age:19,gender:"male"},
{name:"dd",age:20,gender:"female"},
];
// 創(chuàng)建表格大概樣式
var table=document.createElement("table");
var tbody=document.createElement("tbody");
var thead=document.createElement("thead");
table.appendChild(tbody);
table.appendChild(thead);
var trHead=document.createElement("tr");
thead.appendChild(trHead);
// 創(chuàng)建表頭
for(var k in data[0]){
var th=document.createElement("th");
th.appendChild(document.createTextNode(k));
trHead.appendChild(th)
}
//創(chuàng)建每行的tr和td
for(var i=0;i<data.length;i++){
var tr=document.createElement("tr");
for( var k in data[i]){
var td=document.createElement("td");
td.appendChild(document.createTextNode(data[i][k]));
tr.appendChild(td);
td.width="100";
td.height="40";
}
tr.align="center";
tbody.appendChild(tr);
}
table.border="1";
//加到頁面中
document.body.appendChild(table);
眾所周知JavaScript是一門單線程的語言,所以在JavaScript的世界中默認(rèn)的情況下,同一個時間節(jié)點只能做一件事情,這樣的設(shè)定就造成了JavaScript這門語言的一些局限性,比如在我們的頁面中加載一些遠(yuǎn)程數(shù)據(jù)時,如果按照單線程同步的方式運行,一旦有HTTP請求向服務(wù)器發(fā)送,就會出現(xiàn)等待數(shù)據(jù)返回之前網(wǎng)頁假死的效果出現(xiàn)。因為JavaScript在同一個時間只能做一件事,這就導(dǎo)致了頁面渲染和事件的執(zhí)行,在這個過程中無法進(jìn)行。顯然在實際的開發(fā)中我們并沒有遇見過這種情況。
基于以上的描述,我們知道在JavaScript的世界中,應(yīng)該存在一種解決方案,來處理單線程造成的詬病。這就是同步【阻塞】和異步【非阻塞】執(zhí)行模式的出現(xiàn)。
同步(阻塞):
同步的意思是JavaScript會嚴(yán)格按照單線程(從上到下、從左到右的方式)執(zhí)行代碼邏輯,進(jìn)行代碼的解釋和運行,所以在運行代碼時,不會出現(xiàn)先運行4、5行的代碼,再回頭運行1、3行的代碼這種情況。比如下列操作。
var a=1
var b=2
var c=a + b
//這個例子總c一定是3不會出現(xiàn)先執(zhí)行第三行然后在執(zhí)行第二行和第一行的情況
console.log(c)
接下來通過下列的案例升級一下代碼的運行場景:
var a=1
var b=2
var d1=new Date().getTime()
var d2=new Date().getTime()
while(d2-d1<2000){
d2=new Date().getTime()
}
//這段代碼在輸出結(jié)果之前網(wǎng)頁會進(jìn)入一個類似假死的狀態(tài)
console.log(a+b)
當(dāng)我們按照順序執(zhí)行上面代碼時,我們的代碼在解釋執(zhí)行到第4行時,還是正常的速度執(zhí)行,但是在下一行就會進(jìn)入一個持續(xù)的循環(huán)中。d2和d1在行級間的時間差僅僅是毫秒內(nèi)的差別,所以在執(zhí)行到while循環(huán)的時候d2-d1的值一定比2000小,那么這個循環(huán)會執(zhí)行到什么時候呢?由于每次循環(huán)時,d2都會獲取一次當(dāng)前的時間發(fā)生變化,直到d2-d1==2000等情況,這時也就是正好過了2秒的時間,我們的程序才能跳出循環(huán),進(jìn)而再輸出a+b的結(jié)果。那么這段程序的實際執(zhí)行時間至少是2秒以上。這就導(dǎo)致了程序阻塞的出現(xiàn),這也是為什么將同步的代碼運行機(jī)制叫做阻塞式運行的原因。
阻塞式運行的代碼,在遇到消耗時間的代碼片段時,之后的代碼都必須等待耗時的代碼運行完畢,才能得到執(zhí)行資源,這就是單線程同步的特點。
異步(非阻塞):
在上面的闡述中,我們明白了單線程同步模型中的問題所在,接下來引入單線程異步模型的介紹。異步的意思就是和同步對立,所以異步模式的代碼是不會按照默認(rèn)順序執(zhí)行的。JavaScript執(zhí)行引擎在工作時,仍然是按照從上到下從左到右的方式解釋和運行代碼。在解釋時,如果遇到異步模式的代碼,引擎會將當(dāng)前的任務(wù)“掛起”并略過。也就是先不執(zhí)行這段代碼,繼續(xù)向下運行非異步模式的代碼,那么什么時候來執(zhí)行異步代碼呢?直到同步代碼全部執(zhí)行完畢后,程序會將之前“掛起”的異步代碼按照“特定的順序”來進(jìn)行執(zhí)行,所以異步代碼并不會【阻塞】同步代碼的運行,并且異步代碼并不是代表進(jìn)入新的線程同時執(zhí)行,而是等待同步代碼執(zhí)行完畢再進(jìn)行工作。我們閱讀下面的代碼分析:
var a=1
var b=2
setTimeout(function(){
console.log('輸出了一些內(nèi)容')
},2000)
//這段代碼會直接輸出3并且等待2秒左右的時間在輸出function內(nèi)部的內(nèi)容
console.log(a+b)
這段代碼的setTimeout定時任務(wù)規(guī)定了2秒之后執(zhí)行一些內(nèi)容,在運行當(dāng)前程序執(zhí)行到setTimeout時,并不會直接執(zhí)行內(nèi)部的回調(diào)函數(shù),而是會先將內(nèi)部的函數(shù)在另外一個位置(具體是什么位置下面會介紹)保存起來,然后繼續(xù)執(zhí)行下面的console.log進(jìn)行輸出,輸出之后代碼執(zhí)行完畢,然后等待大概2秒左右,之前保存的函數(shù)再執(zhí)行。
非阻塞式運行的代碼,程序運行到該代碼片段時,執(zhí)行引擎會將程序保存到一個暫存區(qū),等待所有同步代碼全部執(zhí)行完畢后,非阻塞式的代碼會按照特定的執(zhí)行順序,分步執(zhí)行。這就是單線程異步的特點。
通俗的講:
通俗的講,同步和異步的關(guān)系是這樣的:
【同步的例子】:比如我們在核酸檢測站,進(jìn)行核酸檢測這個流程就是同步的。每個人必須按照來的時間,先后進(jìn)行排隊,而核酸檢測人員會按照排隊人的順序嚴(yán)格的進(jìn)行逐一檢測,在第一個人沒有檢測完成前,第二個人就得無條件等待,這個就是一個阻塞流程。如果排隊過程中第一個人在檢測時出了問題,如棉簽斷了需要換棉簽,這樣更換時間就會追加到這個人身上,直到他順利的檢測完畢,第二個人才能輪到。如果在檢測中間棉簽沒有了,或者是錄入信息的系統(tǒng)崩潰了,整個隊列就進(jìn)入無條件掛起狀態(tài)所有人都做不了了。這就是結(jié)合生活中的同步案例。
【異步的例子】:還是結(jié)合生活中,當(dāng)我們進(jìn)餐館吃飯時,這個場景就屬于一個完美的異步流程場景。每一桌來的客人會按照他們來的順序進(jìn)行點單,假設(shè)只有一個服務(wù)員的情況,點單必須按照先后順序,但是服務(wù)員不需要等第一桌客人點好的菜出鍋上菜,就可以直接去收集第二桌第三桌客人的需求。這樣可能在十分鐘之內(nèi),服務(wù)員就將所有桌的客人點菜的菜單統(tǒng)計出來,并且發(fā)送給了后廚。之后的菜也不會按照點餐顧客的課桌順序,因為后廚收集到菜單之后可能有1,2,3桌的客人都點了鍋包肉,那么他可能會先一次出三份鍋包肉,這樣鍋包肉在上菜的時候1,2,3桌的客人都能得到,并且其他的菜也會亂序的逐一上菜,這個過程就是異步的。如果按照同步的模式點餐,默認(rèn)在飯店點菜就會出現(xiàn)飯店在第一桌客人上滿菜之前第二桌之后的客人就只能等待連單都不能點的狀態(tài)。
總結(jié):
JavaScript的運行順序就是完全單線程的異步模型:同步在前,異步在后。所有的異步任務(wù)都要等待當(dāng)前的同步任務(wù)執(zhí)行完畢之后才能執(zhí)行。請看下面的案例:
var a=1
var b=2
var d1=new Date().getTime()
var d2=new Date().getTime()
setTimeout(function(){
console.log('我是一個異步任務(wù)')
},1000)
while(d2-d1<2000){
d2=new Date().getTime()
}
//這段代碼在輸出3之前會進(jìn)入假死狀態(tài),'我是一個異步任務(wù)'一定會在3之后輸出
console.log(a+b)
觀察上面的程序我們實際運行之后就會感受到單線程異步模型的執(zhí)行順序了,并且這里我們會發(fā)現(xiàn)setTimeout設(shè)置的時間是1000毫秒但是在while的阻塞2000毫秒的循環(huán)之后并沒有等待1秒而是直接輸出了我是一個異步任務(wù),這是因為setTimout的時間計算是從setTimeout()這個函數(shù)執(zhí)行時開始計算的。
上面我們通過幾個簡單的例子大概了解了一下JS的運行順序,那么為什么是這個順序,這個順序的執(zhí)行原理是什么樣的,我們應(yīng)該如何更好更深的探究真相呢?這里需要介紹一下瀏覽器中一個Tab頁面的實際線程組成。
在了解線程組成前要了解一點,雖然瀏覽器是單線程執(zhí)行JavaScript代碼的,但是瀏覽器實際是以多個線程協(xié)助操作來實現(xiàn)單線程異步模型的,具體線程組成如下:
按照真實的瀏覽器線程組成分析,我們會發(fā)現(xiàn)實際上運行JavaScript的線程其實并不是一個,但是為什么說JavaScript是一門單線程的語言呢?因為這些線程中實際參與代碼執(zhí)行的線程并不是所有線程,比如GUI渲染線程為什么單獨存在,這個是防止我們在html網(wǎng)頁渲染一半的時候突然執(zhí)行了一段阻塞式的JS代碼而導(dǎo)致網(wǎng)頁卡在一半停住這種效果。在JavaScript代碼運行的過程中實際執(zhí)行程序時,同時只存在一個活動線程,這里實現(xiàn)同步異步就是靠多線程切換的形式來進(jìn)行實現(xiàn)的。
所以我們通常分析時,將上面的細(xì)分線程歸納為下列兩條線程:
上圖是JavaScript運行時的一個工作流程和內(nèi)存劃分的簡要描述,我們根據(jù)圖中可以得知主線程就是我們JavaScript執(zhí)行代碼的線程,主線程代碼在運行時,會按照同步和異步代碼將其分成兩個去處,如果是同步代碼執(zhí)行,就會直接將該任務(wù)放在一個叫做“函數(shù)執(zhí)行棧”的空間進(jìn)行執(zhí)行,執(zhí)行棧是典型的【棧結(jié)構(gòu)】(先進(jìn)后出),程序在運行的時候會將同步代碼按順序入棧,將異步代碼放到【工作線程】中暫時掛起,【工作線程】中保存的是定時任務(wù)函數(shù)、JS的交互事件、JS的網(wǎng)絡(luò)請求等耗時操作。
當(dāng)【主線程】將代碼塊篩選完畢后,進(jìn)入執(zhí)行棧的函數(shù)會按照從外到內(nèi)的順序依次運行,運行中涉及到的對象數(shù)據(jù)是在堆內(nèi)存中進(jìn)行保存和管理的。當(dāng)執(zhí)行棧內(nèi)的任務(wù)全部執(zhí)行完畢后,執(zhí)行棧就會清空。執(zhí)行棧清空后,“事件循環(huán)”就會工作,“事件循環(huán)”會檢測【任務(wù)隊列】中是否有要執(zhí)行的任務(wù),那么這個任務(wù)隊列的任務(wù)來源就是工作線程,程序運行期間,工作線程會把到期的定時任務(wù)、返回數(shù)據(jù)的http任務(wù)等【異步任務(wù)】按照先后順序插入到【任務(wù)隊列】中,等執(zhí)行棧清空后,事件循環(huán)會訪問任務(wù)隊列,將任務(wù)隊列中存在的任務(wù),按順序(先進(jìn)先出)放在執(zhí)行棧中繼續(xù)執(zhí)行,直到任務(wù)隊列清空。
function task1(){
console.log('第一個任務(wù)')
}
function task2(){
console.log('第二個任務(wù)')
}
function task3(){
console.log('第三個任務(wù)')
}
function task4(){
console.log('第四個任務(wù)')
}
task1()
setTimeout(task2,1000)
setTimeout(task3,500)
task4()
剛才的文字閱讀可能在大腦中很難形成一個帶動畫的圖形界面來幫助我們分析JavaScript的實際運行思路,接下來我們將這段代碼肢解之后詳細(xì)的研究一下。
按照字面分析:
按照字面分析,我們創(chuàng)建了四個函數(shù)代表4個任務(wù),函數(shù)本身都是同步代碼。在執(zhí)行的時候會按照1,2,3,4進(jìn)行解析,解析過程中我們發(fā)現(xiàn)任務(wù)2和任務(wù)3被setTimeout進(jìn)行了定時托管,這樣就只能先運行任務(wù)1和任務(wù)4了。當(dāng)任務(wù)1和任務(wù)4運行完畢之后500毫秒后運行任務(wù)3,1000毫米后運行任務(wù)2。
那么他們在實際運行時又是經(jīng)歷了怎么樣的流程來運行的呢?大概的流程我們以圖解的形式分析一下。
圖解分析:
如上圖,在上述代碼剛開始運行的時候我們的主線程即將工作,按照順序從上到下進(jìn)行解釋執(zhí)行,此時執(zhí)行棧、工作線程、任務(wù)隊列都是空的,事件循環(huán)也沒有工作。接下來我們分析下一個階段程序做了什么事情。
結(jié)合上圖可以看出程序在主線程執(zhí)行之后就將任務(wù)1、4和任務(wù)2、3分別放進(jìn)了兩個方向,任務(wù)1和任務(wù)4都是立即執(zhí)行任務(wù)所以會按照1->4的順序進(jìn)棧出棧(這里由于任務(wù)1和4是平行任務(wù)所以會先執(zhí)行任務(wù)1的進(jìn)出棧再執(zhí)行任務(wù)4的進(jìn)出棧),而任務(wù)2和任務(wù)3由于是異步任務(wù)就會進(jìn)入工作線程掛起并開始計時,并不影響主線程運行,此時的任務(wù)隊列還是空置的。
我們發(fā)現(xiàn)同步任務(wù)的執(zhí)行速度是飛快的,這樣一下執(zhí)行棧已經(jīng)空了,而任務(wù)2和任務(wù)3還沒有到時間,這樣我們的事件循環(huán)就會開始工作等待任務(wù)隊列中的任務(wù)進(jìn)入,接下來就是執(zhí)行異步任務(wù)的時候了。
我們發(fā)現(xiàn)任務(wù)隊列并不是一下子就會將任務(wù)2和任務(wù)三一起放進(jìn)去,而是哪個計時器到時間了哪個放進(jìn)去,這樣我們的事件循環(huán)就會發(fā)現(xiàn)隊列中的任務(wù),并且將任務(wù)拿到執(zhí)行棧中進(jìn)行消費,此時會輸出任務(wù)3的內(nèi)容。
到這就是最后一次執(zhí)行,當(dāng)執(zhí)行完畢后工作線程中沒有計時任務(wù),任務(wù)隊列的任務(wù)清空程序到此執(zhí)行完畢。
我們通過圖解之后腦子里就會更清晰的能搞懂異步任務(wù)的執(zhí)行方式了,這里采用最簡單的任務(wù)模型進(jìn)行描繪復(fù)雜的任務(wù)在內(nèi)存中的分配和走向是非常復(fù)雜的,我們有了這次的經(jīng)驗之后就可以通過觀察代碼在大腦中先模擬一次執(zhí)行,這樣可以更清晰的理解JS的運行機(jī)制。
執(zhí)行棧是一個棧的數(shù)據(jù)結(jié)構(gòu),當(dāng)我們運行單層函數(shù)時,執(zhí)行棧執(zhí)行的函數(shù)進(jìn)棧后,會出棧銷毀然后下一個進(jìn)棧下一個出棧,當(dāng)有函數(shù)嵌套調(diào)用的時候棧中就會堆積棧幀,比如我們查看下面的例子:
function task1(){
console.log('task1執(zhí)行')
task2()
console.log('task2執(zhí)行完畢')
}
function task2(){
console.log('task2執(zhí)行')
task3()
console.log('task3執(zhí)行完畢')
}
function task3(){
console.log('task3執(zhí)行')
}
task1()
console.log('task1執(zhí)行完畢')
我們根據(jù)字面閱讀就能很簡單的分析出輸出的結(jié)果會是
/*
task1執(zhí)行
task2執(zhí)行
task3執(zhí)行
task3執(zhí)行完畢
task2執(zhí)行完畢
task1執(zhí)行完畢
*/
那么這種嵌套函數(shù)在執(zhí)行棧中的操作流程是什么樣的呢?
第一次執(zhí)行的時候調(diào)用task1函數(shù)執(zhí)行到console.log的時候先進(jìn)行輸出,接下來會遇到task2函數(shù)的調(diào)用會出現(xiàn)下面的情況:
執(zhí)行到此時檢測到task2中還有調(diào)用task3的函數(shù),那么就會繼續(xù)進(jìn)入task3中執(zhí)行,如下圖:
在執(zhí)行完task3中的輸出之后task3內(nèi)部沒有其他代碼,那么task3函數(shù)就算執(zhí)行完畢那么就會發(fā)生出棧工作。
此時我們會發(fā)現(xiàn)task3出棧之后程序運行又會回到task2的函數(shù)中繼續(xù)他的執(zhí)行。接下來會發(fā)生相同的事情。
再之后就剩下task1自己了,他在task2銷毀之后輸出task2執(zhí)行完畢后他也會隨著出棧而銷毀。
當(dāng)task1執(zhí)行完畢之后它隨著銷毀最后一行輸出,就會進(jìn)入執(zhí)行棧執(zhí)行并銷毀,銷毀之后執(zhí)行棧和主線程清空。這個過程就會出現(xiàn)123321的這個順序,而且我們在打印輸出時,也能通過打印的順序來理解入棧和出棧的順序和流程。
關(guān)于上面的執(zhí)行棧執(zhí)行邏輯清楚后,我們就順便學(xué)習(xí)一下遞歸函數(shù),遞歸函數(shù)是項目開發(fā)時經(jīng)常涉及到的場景。我們經(jīng)常會在未知深度的樹形結(jié)構(gòu),或其他合適的場景中使用遞歸。那么遞歸在面試中也會經(jīng)常被問到風(fēng)險問題,如果了解了執(zhí)行棧的執(zhí)行邏輯后,遞歸函數(shù)就可以看成是在一個函數(shù)中嵌套n層執(zhí)行,那么在執(zhí)行過程中會觸發(fā)大量的棧幀堆積,如果處理的數(shù)據(jù)過大,會導(dǎo)致執(zhí)行棧的高度不夠放置新的棧幀,而造成棧溢出的錯誤。所以我們在做海量數(shù)據(jù)遞歸的時候一定要注意這個問題。
關(guān)于執(zhí)行棧的深度:
執(zhí)行棧的深度根據(jù)不同的瀏覽器和JS引擎有著不同的區(qū)別,我們這里就Chrome瀏覽器為例子來嘗試一下遞歸的溢出:
var i=0;
function task(){
let index=i++
console.log(`遞歸了${index}次`)
task()
console.log(`第${index}次遞歸結(jié)束`)
}
task()
我們發(fā)現(xiàn)在遞歸了11378次之后會提示超過棧深度的錯誤,也就是我們無法在Chrome或者其他瀏覽器做太深層的遞歸操作。
發(fā)現(xiàn)問題后,我們再考慮如何能通過技術(shù)手段跨越遞歸的限制。可以將代碼做如下更改,這樣就不會出現(xiàn)遞歸問題了。
var i=0;
function task(){
let index=i++
console.log(`遞歸了${index}次`)
setTimeout(function(){
task()
})
console.log(`第${index}次遞歸結(jié)束`)
}
task()
我們發(fā)現(xiàn)只是做了一個小小的改造,這樣就不會出現(xiàn)溢出的錯誤了。這是為什么呢?
在了解原因之前我們先看控制臺的輸出,結(jié)合控制臺輸出我們發(fā)現(xiàn)確實超過了界限也沒有報錯。
圖解原因:
這個是因為我們這里使用了異步任務(wù)去調(diào)用遞歸中的函數(shù),那么這個函數(shù)在執(zhí)行的時候就不只使用棧進(jìn)行執(zhí)行了。
先看沒有異步流程時候的執(zhí)行圖例:
再看有了異步任務(wù)的遞歸:
有了異步任務(wù)之后我們的遞歸就不會疊加棧幀了,因為放入工作線程之后該函數(shù)就結(jié)束了,可以出棧銷毀,那么在執(zhí)行棧中就永遠(yuǎn)都是只有一個任務(wù)在運行,這樣就防止了棧幀的無限疊加,從而解決了無限遞歸的問題,不過異步遞歸的過程是無法保證運行速度的,在實際的工作場景中,如果考慮性能問題,還需要使用 while 循環(huán)等解決方案,來保證運行效率的問題,在實際工作場景中,盡量避免遞歸循環(huán),因為遞歸循環(huán)就算控制在有限棧幀的疊加,其性能也遠(yuǎn)遠(yuǎn)不及指針循環(huán)。
在明確了事件循環(huán)模型以及JavaScript的執(zhí)行流程后,我們認(rèn)識了一個叫做任務(wù)隊列的容器,他的數(shù)據(jù)結(jié)構(gòu)式隊列的結(jié)構(gòu)。所有除同步任務(wù)外的代碼都會在工作線程中,按照他到達(dá)的時間節(jié)點有序的進(jìn)入任務(wù)隊列,而且任務(wù)隊列中的異步任務(wù)又分為【宏任務(wù)】和【微任務(wù)】。
在了解【宏任務(wù)】和【微任務(wù)】前,還是哪生活中的實際場景舉個例子:
比如: 在去銀行辦理業(yè)務(wù)時,每個人都需要在進(jìn)入銀行時找到取票機(jī)進(jìn)行取票,這個操作會把來辦理業(yè)務(wù)的人按照取票的順序排成一個有序的隊列。假設(shè)銀行只開通了一個辦事窗口,窗口的工作人員會按照排隊的順序進(jìn)行叫號,到達(dá)號碼的人就可以前往窗口辦理業(yè)務(wù),在第一個人辦理業(yè)務(wù)的過程中,第二個以后的人都需要進(jìn)行等待。
這個場景與JavaScript的異步任務(wù)隊列執(zhí)行場景是一模一樣的,如果把每個辦業(yè)務(wù)的人當(dāng)作JavaScript中的每一個異步的任務(wù),那么取號就相當(dāng)于將異步任務(wù)放入任務(wù)隊列。銀行的窗口就相當(dāng)于【函數(shù)執(zhí)行棧】,在叫號時代表將當(dāng)前隊列的第一個任務(wù)放入【函數(shù)執(zhí)行棧】運行。這時可能每個人在窗口辦理的業(yè)務(wù)內(nèi)容各不相同,比如第一個人僅僅進(jìn)行開卡的操作,這樣銀行工作人員就會為其執(zhí)行開卡流程,這就相當(dāng)于執(zhí)行異步任務(wù)內(nèi)部的代碼。
如果第一個人的銀行卡開通完畢,銀行的工作人員不會立即叫第二個人過來,而是會詢問第一個人,“您是否需要為剛才開通的卡辦理一些增值業(yè)務(wù),比如做個活期儲蓄。”,這時相當(dāng)于在原始開卡的業(yè)務(wù)流程中臨時追加了一個新的任務(wù),按照JavaScript的執(zhí)行順序,這個人的新任務(wù)應(yīng)該回到取票機(jī)拿取一張新的號碼,并且在隊尾重新排隊,這樣工作的話辦事效率就會急劇下降。所以銀行實際的做法是在叫下一個人辦理業(yè)務(wù)前,如果前面的人臨時有新的業(yè)務(wù)要辦理,工作人員會繼續(xù)為其辦理業(yè)務(wù),直到這個人的所有事情都辦理完畢。
從取卡到辦理追加業(yè)務(wù)完成的這個過程,就是微任務(wù)的實際體現(xiàn)。在JavaScript運行環(huán)境中,包括主線程代碼在內(nèi),可以理解為所有的任務(wù)內(nèi)部都存在一個微任務(wù)隊列,在每下一個宏任務(wù)執(zhí)行前,事件循環(huán)系統(tǒng)都會先檢測當(dāng)前的代碼塊中是否包含已經(jīng)注冊的微任務(wù),并將隊列中的微任務(wù)優(yōu)先執(zhí)行完畢,進(jìn)而執(zhí)行下一個宏任務(wù)。所以實際的任務(wù)隊列的結(jié)構(gòu)是這樣的,如圖:
由上述內(nèi)容得知JavaScript中存在兩種異步任務(wù),一種是宏任務(wù)一種是微任務(wù),他們的特點如下:
宏任務(wù):
宏任務(wù)是JavaScript中最原始的異步任務(wù),包括setTimeout、setInterval、AJAX等,在代碼執(zhí)行環(huán)境中按照同步代碼的順序,逐個進(jìn)入工作線程掛起,再按照異步任務(wù)到達(dá)的時間節(jié)點,逐個進(jìn)入異步任務(wù)隊列,最終按照隊列中的順序進(jìn)入函數(shù)執(zhí)行棧進(jìn)行執(zhí)行。
微任務(wù):
微任務(wù)是隨著ECMA標(biāo)準(zhǔn)升級提出的新的異步任務(wù),微任務(wù)在異步任務(wù)隊列的基礎(chǔ)上增加了【微任務(wù)】的概念,每一個宏任務(wù)執(zhí)行前,程序會先檢測其中是否有當(dāng)次事件循環(huán)未執(zhí)行的微任務(wù),優(yōu)先清空本次的微任務(wù)后,再執(zhí)行下一個宏任務(wù),每一個宏任務(wù)內(nèi)部可注冊當(dāng)次任務(wù)的微任務(wù)隊列,再下一個宏任務(wù)執(zhí)行前運行,微任務(wù)也是按照進(jìn)入隊列的順序執(zhí)行的。
總結(jié):
在JavaScript的運行環(huán)境中,代碼的執(zhí)行流程是這樣的:
# | 瀏覽器 | Node |
I/O | ? | ? |
setTimeout | ? | ? |
setInterval | ? | ? |
setImmediate | ? | ? |
requestAnimationFrame | ? | ? |
有些地方會列出來UI Rendering,說這個也是宏任務(wù),可是在讀了HTML規(guī)范文檔以后,發(fā)現(xiàn)這很顯然是和微任務(wù)平行的一個操作步驟 requestAnimationFrame姑且也算是宏任務(wù)吧,requestAnimationFrame在MDN的定義為,下次頁面重繪前所執(zhí)行的操作,而重繪也是作為宏任務(wù)的一個步驟來存在的,且該步驟晚于微任務(wù)的執(zhí)行
# | 瀏覽器 | Node |
process.nextTick | ? | ? |
MutationObserver | ? | ? |
Promise.then catch finally | ? | ? |
代碼輸出順序問題1
setTimeout(function() {console.log('timer1')}, 0)
requestAnimationFrame(function(){
console.log('UI update')
})
setTimeout(function() {console.log('timer2')}, 0)
new Promise(function executor(resolve) {
console.log('promise 1')
resolve()
console.log('promise 2')
}).then(function() {
console.log('promise then')
})
console.log('end')
解析:
本案例輸出的結(jié)果為:猜對我就告訴你,先思考,猜對之后結(jié)合運行結(jié)果分析。
按照同步先行,異步靠后的原則,閱讀代碼時,先分析同步代碼和異步代碼,Promise對象雖然是微任務(wù),但是new Promise時的回調(diào)函數(shù)是同步執(zhí)行的,所以優(yōu)先輸出promise 1 和 promise 2。
在resolve執(zhí)行時Promise對象的狀態(tài)變更為已完成,所以then函數(shù)的回調(diào)被注冊到微任務(wù)事件中,此時并不執(zhí)行,所以接下來應(yīng)該輸出end。
同步代碼執(zhí)行結(jié)束后,觀察異步代碼的宏任務(wù)和微任務(wù),在本次的同步代碼塊中注冊的微任務(wù)會優(yōu)先執(zhí)行,參考上文中描述的列表,Promise為微任務(wù),setTimeout和requestAnimationFrame為宏任務(wù),所以Promise的異步任務(wù)會在下一個宏任務(wù)執(zhí)行前執(zhí)行,所以promise then是第四個輸出的結(jié)果。
接下來參考setTimeout和requestAnimationFrame兩個宏任務(wù),這里的運行結(jié)果是多種情況。如果三個宏任務(wù)都為setTimeout的話會按照代碼編寫的順序執(zhí)行宏任務(wù),而中間包含了一個requestAnimationFrame ,這里就要學(xué)習(xí)一下他們的執(zhí)行時機(jī)了。setTimeout是在程序運行到setTimeout時立即注冊一個宏任務(wù),所以兩個setTimeout的順序一定是固定的timer1和timer2會按照順序輸出。而requestAnimationFrame是請求下一次重繪事件,所以他的執(zhí)行頻率要參考瀏覽器的刷新率。
參考如下代碼:
let i=0;
let d=new Date().getTime()
let d1=new Date().getTime()
function loop(){
d1=new Date().getTime()
i++
//當(dāng)間隔時間超過1秒時執(zhí)行
if((d1-d)>=1000){
d=d1
console.log(i)
i=0
console.log('經(jīng)過了1秒')
}
requestAnimationFrame(loop)
}
loop()
該代碼在瀏覽器運行時,控制臺會每間隔1秒進(jìn)行一次輸出,輸出的i就是loop函數(shù)執(zhí)行的次數(shù),如下圖:
這個輸出意味著requestAnimationFrame函數(shù)的執(zhí)行頻率是每秒鐘60次左右,他是按照瀏覽器的刷新率來進(jìn)行執(zhí)行的,也就是當(dāng)屏幕刷新一次時該函數(shù)就會觸發(fā)一次,相當(dāng)于運行間隔是16毫秒左右。
繼續(xù)參考下列代碼:
let i=0;
let d=new Date().getTime()
let d1=new Date().getTime()
function loop(){
d1=new Date().getTime()
i++
if((d1-d)>=1000){
d=d1
console.log(i)
i=0
console.log('經(jīng)過了1秒')
}
setTimeout(loop,0)
}
loop()
該代碼結(jié)構(gòu)與上面的案例類似,循環(huán)是采用setTimeout進(jìn)行控制的,所以參考運行結(jié)果,如圖:
根據(jù)運行結(jié)果得知,setTimeout(fn,0)的執(zhí)行頻率是每秒執(zhí)行200次左右,所以他的間隔是5毫秒左右。
由于這兩個異步的宏任務(wù)出發(fā)時機(jī)和執(zhí)行頻率不同,會導(dǎo)致三個宏任務(wù)的觸發(fā)結(jié)果不同,如果我們打開網(wǎng)頁時,恰好趕上5毫秒內(nèi)執(zhí)行了網(wǎng)頁的重繪事件,requestAnimationFrame在工作線程中就會到達(dá)觸發(fā)時機(jī)優(yōu)先進(jìn)入任務(wù)隊列,所以此時會輸出:UI update->timer1->timer2。
而當(dāng)打開網(wǎng)頁時上一次的重繪剛結(jié)束,下一次重繪的觸發(fā)是16毫秒后,此時setTimeout注冊的兩個任務(wù)在工作線程中就會優(yōu)先到達(dá)觸發(fā)時機(jī),這時輸出的結(jié)果是:timer1->timer2->UI update。
所以此案例的運行結(jié)果如下2圖所示:
代碼輸出順序問題2
document.addEventListener('click', function(){
Promise.resolve().then(()=> console.log(1));
console.log(2);
})
document.addEventListener('click', function(){
Promise.resolve().then(()=> console.log(3));
console.log(4);
})
解析:仍然是猜對了告訴你哈~,先運行一下試試吧。
這個案例代碼簡單易懂,但是很容易引起錯誤答案的出現(xiàn)。由于該事件是直接綁定在document上的,所以點擊網(wǎng)頁就會觸發(fā)該事件,在代碼運行時相當(dāng)于按照順序注冊了兩個點擊事件,兩個點擊事件會被放在工作線程中實時監(jiān)聽觸發(fā)時機(jī),當(dāng)元素被點擊時,兩個事件會按照先后的注冊順序放入異步任務(wù)隊列中進(jìn)行執(zhí)行,所以事件1和事件2會按照代碼編寫的順序觸發(fā)。
這里就會導(dǎo)致有人分析出錯誤答案:2,4,1,3。
為什么不是2,4,1,3呢?由于事件執(zhí)行時并不會阻斷JS默認(rèn)代碼的運行,所以事件任務(wù)也是異步任務(wù),并且是宏任務(wù),所以兩個事件相當(dāng)于按順序執(zhí)行的兩個宏任務(wù)。
這樣就會分出兩個運行環(huán)境,第一個事件執(zhí)行時,console.log(2);是第一個宏任務(wù)中的同步代碼,所以他會立即執(zhí)行,而Promise.resolve().then(()=> console.log(1));屬于微任務(wù),他會在下一個宏任務(wù)觸發(fā)前執(zhí)行,所以這里輸出2后會直接輸出1.
而下一個事件的內(nèi)容是相同道理,所以輸出順序為:2,1,4,3。
關(guān)于事件循環(huán)模型今天就介紹到這里,在NodeJS中的事件循環(huán)模型和瀏覽器中是不一樣的,本文是以瀏覽器的事件循環(huán)模型為基礎(chǔ)進(jìn)行介紹,事件循環(huán)系統(tǒng)在JavaScript異步編程中占據(jù)的比重是非常大的,在工作中可使用場景也是眾多的,掌握了事件循環(huán)模型就相當(dāng)于,異步編程的能力上升了一個新的高度。
1. 什么是單線程
主程序只有一個線程,即同一時間片斷內(nèi)其只能執(zhí)行單個任務(wù)。
2. 為什么選擇單線程?
JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復(fù)雜的同步問題。
3. 單線程意味著什么?
單線程就意味著,所有任務(wù)都需要排隊,前一個任務(wù)結(jié)束,才會執(zhí)行后一個任務(wù)。如果前一個任務(wù)耗時很長,后一個任務(wù)就需要一直等著。這就會導(dǎo)致IO操作(耗時但cpu閑置)時造成性能浪費的問題。
4. 如何解決單線程帶來的性能問題?
答案是異步!主線程完全可以不管IO操作,暫時掛起處于等待中的任務(wù),先運行排在后面的任務(wù)。等到IO操作返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。于是,所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)
注: 當(dāng)主線程阻塞時,任務(wù)隊列仍然是能夠被推入任務(wù)的
1. JavaScript 內(nèi)存模型
講事件循環(huán)之前,先看一張下網(wǎng)上看到的 JavaScript 內(nèi)存模型,相信看完這個會對事件循環(huán)機(jī)制有一種豁然開朗的感覺。
調(diào)用棧(Call Stack):用于主線程任務(wù)的執(zhí)行
堆(Heap): 用于存放非結(jié)構(gòu)化數(shù)據(jù),譬如程序分配的變量與對象
任務(wù)隊列(Queue): 用于存放異步任務(wù)與定時任務(wù)。
2. JavaScript 代碼執(zhí)行機(jī)制:
所有同步任務(wù)都在主線程上的棧中執(zhí)行。
主線程之外,還存在一個"任務(wù)隊列"(task queue)。只要異步任務(wù)有了運行結(jié)果,就在"任務(wù)隊列"之中放置一個事件。
一旦"棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會讀取"任務(wù)隊列",選擇出需要首先執(zhí)行的任務(wù)(由瀏覽器決定,并不按序)。
3. Event Loop
現(xiàn)在我們來聊事件循環(huán)。事件循環(huán)顧名思義它就是一個循環(huán),主線程會不斷循環(huán)執(zhí)行上面的第三步,其基本的代碼邏輯如下所示:
4. 常見異步任務(wù)進(jìn)入任務(wù)隊列時機(jī)
行為 | 時機(jī) |
---|---|
DOM操作 | 在用戶點擊等操作事件完成后 |
網(wǎng)絡(luò)操作(Ajax等) | 在網(wǎng)絡(luò)操作響應(yīng)后 |
定時器 | 在規(guī)定時間到達(dá)后 |
事件循環(huán)機(jī)制圖解:
MacroTask(Task)
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
MicroTask(在ES2015規(guī)范中稱為Job)
process.nextTick, Promise, Object.observe, MutationObserver
規(guī)范:
每個瀏覽器環(huán)境,至多有一個event loop。
一個event loop可以有1個或多個task queue,而僅有一個 MicroTask Queue。
一個task queue是一列有序的task, 每個task定義時都有一個task source,從同一個task source來的task必須放到同一個task queue,從不同源來的則被添加到不同隊列。
tasks are scheduled,所以瀏覽器可以從內(nèi)部到JS/DOM,保證動作按序發(fā)生。
Microtasks are scheduled,Microtask queue 在當(dāng)前 task queue 的結(jié)尾執(zhí)行。microtask中添加的microtask也被添加到Microtask queue的末尾并處理。
注: event loop的每個turn,是由瀏覽器決定先執(zhí)行哪個task queue。這允許瀏覽器為不同的task source設(shè)置不同的優(yōu)先級,比如為用戶交互設(shè)置更高優(yōu)先級來使用戶感覺流暢。
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。