通過學(xué)習(xí)JavaScript赞草,我們都知道它是一門單線程語言讹堤,也就是說,在同一時(shí)刻房资,最多也只有一個(gè)代碼段在執(zhí)行蜕劝,但一次只能執(zhí)行一個(gè)任務(wù),這些任務(wù)形成一個(gè)任務(wù)隊(duì)列排隊(duì)等候執(zhí)行轰异,但前端的某些任務(wù)是非常耗時(shí)的,比如網(wǎng)絡(luò)請(qǐng)求暑始,定時(shí)器和事件監(jiān)聽搭独,如果讓他們和別的任務(wù)一樣,都老老實(shí)實(shí)的排隊(duì)等待執(zhí)行的話廊镜,執(zhí)行效率會(huì)非常的低牙肝,甚至導(dǎo)致頁面的假死。所以嗤朴,瀏覽器為這些耗時(shí)任務(wù)開辟了另外的線程配椭,主要包括http請(qǐng)求線程,瀏覽器定時(shí)觸發(fā)器雹姊,瀏覽器事件觸發(fā)線程股缸,這些任務(wù)是異步的。也就是我們所說的事件循環(huán)機(jī)制吱雏。
從上圖我們可以看出敦姻,js主線程它是有一個(gè)執(zhí)行棧的,所有的js代碼都會(huì)在執(zhí)行棧里運(yùn)行歧杏。在執(zhí)行代碼過程中镰惦,如果遇到一些異步代碼(比如setTimeout,ajax,promise.then以及用戶點(diǎn)擊等操作),那么瀏覽器就會(huì)將這些代碼放到一個(gè)幕后線程中去等待,不阻塞主線程的執(zhí)行犬绒,主線程繼續(xù)執(zhí)行棧中剩余的代碼旺入,當(dāng)幕后線程(background thread)里的代碼準(zhǔn)備好了(比如setTimeout時(shí)間到了,ajax請(qǐng)求得到響應(yīng)),該線程就會(huì)將它的回調(diào)函數(shù)放到任務(wù)隊(duì)列中等待執(zhí)行凯力。而當(dāng)主線程執(zhí)行完棧中的所有代碼后茵瘾,它就會(huì)檢查任務(wù)隊(duì)列是否有任務(wù)要執(zhí)行急膀,如果有任務(wù)要執(zhí)行的話,那么就將該任務(wù)放到執(zhí)行棧中執(zhí)行龄捡。如果當(dāng)前任務(wù)隊(duì)列為空的話卓嫂,它就會(huì)一直循環(huán)等待任務(wù)到來。因此聘殖,這叫做事件循環(huán)晨雳。
事件循環(huán)過程:
javascript在執(zhí)行的時(shí)候,按順序從上往下執(zhí)行奸腺,當(dāng)遇到異步代碼的時(shí)候餐禁,瀏覽器會(huì)把這些異步代碼放到其他執(zhí)行模塊中去執(zhí)行,不阻塞主線程的執(zhí)行突照,當(dāng)執(zhí)行完畢后帮非,將回調(diào)函數(shù)放在任務(wù)隊(duì)列中(taskquene)中,當(dāng)主線程執(zhí)行完棧中的所有代碼后讹蘑,然后會(huì)檢查任務(wù)隊(duì)列中是否有任務(wù)要執(zhí)行末盔,如果有,就將該任務(wù)放到 執(zhí)行棧中執(zhí)行座慰,如果為空陨舱,則一直循環(huán)等待任務(wù)的到來。
因?yàn)槿蝿?wù)隊(duì)列有兩種形式:
macrotask 和 microtask 是異步任務(wù)的兩種分類版仔。在掛起任務(wù)時(shí)游盲,JS 引擎會(huì)將所有任務(wù)按照類別分到這兩個(gè)隊(duì)列中,首先在 macrotask 的隊(duì)列(這個(gè)隊(duì)列也被叫做 task queue)中取出第一個(gè)任務(wù)蛮粮,執(zhí)行完畢后取出 microtask 隊(duì)列中的所有任務(wù)順序執(zhí)行益缎;之后再取 macrotask 任務(wù),周而復(fù)始然想,直至兩個(gè)隊(duì)列的任務(wù)都取完莺奔。
macro-task: script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering
micro-task: process.nextTick, Promises(這里指瀏覽器實(shí)現(xiàn)的原生 Promise), Object.observe, MutationObserver
1.檢查Macrotask 隊(duì)列是否為空,若不為空,則進(jìn)行下一步又沾,若為空弊仪,則跳到3
2.從Macrotask隊(duì)列中取隊(duì)首(在隊(duì)列時(shí)間最長(zhǎng))的任務(wù)進(jìn)去執(zhí)行棧中執(zhí)行(僅僅一個(gè)),執(zhí)行完后進(jìn)入下一步
3.檢查Microtask隊(duì)列是否為空杖刷,若不為空励饵,則進(jìn)入下一步,否則滑燃,跳到1(開始新的事件循環(huán))
4.從Microtask隊(duì)列中取隊(duì)首(在隊(duì)列時(shí)間最長(zhǎng))的任務(wù)進(jìn)去事件隊(duì)列執(zhí)行,執(zhí)行完后役听,跳到3;
現(xiàn)在我們已經(jīng)知道了什么是事件循環(huán)機(jī)制,接下來講幾個(gè)我們需要注意的點(diǎn)典予。
(1)setTimeout的時(shí)間設(shè)置為0甜滨,會(huì)立即執(zhí)行嗎?
答案很顯然瘤袖,不會(huì)衣摩,無論我們?cè)O(shè)置多少的延遲時(shí)間,setTimeout總是會(huì)在 主線程的任務(wù)執(zhí)行完z之后輸出捂敌。
<script>
console.log(1);
setTimeout(function(){console.log(2);}, 0);
console.log(3);
</script>
最后的輸出結(jié)果:
可能有些瀏覽器可能會(huì)有一個(gè)最小延遲時(shí)間艾扮,有的是 15ms,有的是 10ms占婉,所以會(huì)造成一些錯(cuò)覺泡嘴,所以我們?cè)倏匆幌孪旅孢@個(gè)例子:
<script>
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
//具體數(shù)字不定,這取決于你的硬件配置和瀏覽器
for(var i = 0; i < 9999; i ++){
console.log(i);
}
console.log("script end");
</script>
這個(gè)數(shù)字我試了下99999999逆济,結(jié)果電腦就......
(2)Ajax請(qǐng)求是否異步
ajax請(qǐng)求內(nèi)容的時(shí)候是異步的酌予,當(dāng)請(qǐng)求完成后,會(huì)觸發(fā)請(qǐng)求完成的事件奖慌,然后把回調(diào)函數(shù)放入callback queue抛虫,等到主線程執(zhí)行該回調(diào)函數(shù)時(shí)還是單線程的。
這里簡(jiǎn)單說一下Ajax異步請(qǐng)求的過程:
首先說明一下Ajax是局部刷新頁面而不是整體加載頁面升薯,能夠減輕服務(wù)器壓力莱褒,提高用戶體驗(yàn)。
如何使用Ajax涎劈?
第一步:創(chuàng)建xmlhttprequest對(duì)象,var xmlhttp =new XMLHttpRequest();XMLHttpRequest對(duì)象用來和服務(wù)器交換數(shù)據(jù)
var xhttp;
if (window.XMLHttpRequest) {
//現(xiàn)代主流瀏覽器
xhttp = new XMLHttpRequest();
} else {
// 針對(duì)瀏覽器阅茶,比如IE5或IE6
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
第二步:使用xmlhttprequest對(duì)象的open()和send()方法發(fā)送資源請(qǐng)求給服務(wù)器蛛枚。
xmlhttp.open(method,url,async) method包括get 和post,url主要是文件或資源的路徑脸哀,async參數(shù)為true(代表異步)或者false(代表同步)
xhttp.send();使用get方法發(fā)送請(qǐng)求到服務(wù)器蹦浦。
xhttp.send(string);使用post方法發(fā)送請(qǐng)求到服務(wù)器。
post 發(fā)送請(qǐng)求什么時(shí)候能夠使用呢撞蜂?
(1)更新一個(gè)文件或者數(shù)據(jù)庫的時(shí)候盲镶。
(2)發(fā)送大量數(shù)據(jù)到服務(wù)器,因?yàn)閜ost請(qǐng)求沒有字符限制蝌诡。
(3)發(fā)送用戶輸入的加密數(shù)據(jù)溉贿。
get例子:
xhttp.open("GET", "ajax_info.txt", true);
xhttp.open("GET", "index.html", true);
xhttp.open("GET", "demo_get.asp?t=" + Math.random(), true);xhttp.send();
post例子
xhttp.open("POST", "demo_post.asp", true);
xhttp.send();
post表單數(shù)據(jù)需要使用xmlhttprequest對(duì)象的setRequestHeader方法增加一個(gè)HTTP頭。
post表單例子
xhttp.open("POST", "ajax_test.aspx", true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("fname=Henry&lname=Ford");
async參數(shù)
(1)async=true 當(dāng)服務(wù)器準(zhǔn)備響應(yīng)時(shí)將執(zhí)行onreadystatechange函數(shù)浦旱。
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
document.getElementById("demo").innerHTML = xhttp.responseText;
}
};
xhttp.open("GET", "index.aspx", true);
xhttp.send();
(2)async=false 則將不需要寫onreadystatechange函數(shù)宇色,直接在send后面寫上執(zhí)行代碼。
xhttp.open("GET", "index.aspx", false);
xhttp.send();
document.getElementById("demo").innerHTML = xhttp.responseText;
第三步:使用xmlhttprequest對(duì)象的responseText或responseXML屬性獲得服務(wù)器的響應(yīng)蘸吓。
使用responseText屬性得到服務(wù)器響應(yīng)的字符串?dāng)?shù)據(jù)维哈,使用responseXML屬性得到服務(wù)器響應(yīng)的XML數(shù)據(jù)。
例子如下:
document.getElementById("demo").innerHTML = xhttp.responseText;
服務(wù)器響應(yīng)的XML數(shù)據(jù)需要使用XML對(duì)象進(jìn)行轉(zhuǎn)換邮绿。
例子:
xmlDoc = xhttp.responseXML;
txt = "";
x = xmlDoc.getElementsByTagName("ARTIST");
for (i = 0; i < x.length; i++) {
txt += x[i].childNodes[0].nodeValue + "<br>";
}
document.getElementById("demo").innerHTML = txt;
第四步:onreadystatechange函數(shù)抢蚀,當(dāng)發(fā)送請(qǐng)求到服務(wù)器镀层,我們想要服務(wù)器響應(yīng)執(zhí)行一些功能就需要使用onreadystatechange函數(shù),每次xmlhttprequest對(duì)象的readyState發(fā)生改變都會(huì)觸發(fā)onreadystatechange函數(shù)皿曲。
onreadystatechange屬性存儲(chǔ)一個(gè)當(dāng)readyState發(fā)生改變時(shí)自動(dòng)被調(diào)用的函數(shù)唱逢。
readyState屬性,XMLHttpRequest對(duì)象的狀態(tài)谷饿,改變從0到4惶我,0代表請(qǐng)求未被初始化,1代表服務(wù)器連接成功博投,2請(qǐng)求被服務(wù)器接收绸贡,3處理請(qǐng)求,4請(qǐng)求完成并且響應(yīng)準(zhǔn)備毅哗。
status屬性听怕,200表示成功響應(yīng),404表示頁面不存在虑绵。
在onreadystatechange事件中尿瞭,服務(wù)器響應(yīng)準(zhǔn)備的時(shí)候發(fā)生,當(dāng)readyState==4和status==200的時(shí)候服務(wù)器響應(yīng)準(zhǔn)備翅睛。
例子:
function loadDoc() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
document.getElementById("demo").innerHTML = xhttp.responseText;
}
};
xhttp.open("GET", "ajax_info.txt", true);
xhttp.send();
}
//函數(shù)作為參數(shù)調(diào)用
<!DOCTYPE html>
<html>
<body>
<p id="demo">Let AJAX change this text.</p>
<button type="button"
onclick="loadDoc('index.aspx', myFunction)">Change Content
</button>
<script>
function loadDoc(url, cfunc) {
var xhttp;
xhttp=new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
cfunc(xhttp);
}
};
xhttp.open("GET", url, true);
xhttp.send();
}
function myFunction(xhttp) {
document.getElementById("demo").innerHTML = xhttp.responseText;
}
</script>
</body>
</html>
(3)界面渲染線程是單獨(dú)開辟的線程声搁,是不是DOM一變化,界面就立刻重新渲染捕发?
如果DOM一變化疏旨,界面就立刻重新渲染,效率必然很低扎酷,所以瀏覽器的機(jī)制規(guī)定界面渲染線程和主線程是互斥的檐涝,主線程執(zhí)行任務(wù)時(shí),瀏覽器渲染線程處于掛起狀態(tài)法挨。
好谁榜,再寫一個(gè)例子總結(jié)說明一下事件循環(huán)機(jī)制
test:
console.log(1)
setTimeout(function() {
//settimeout1
console.log(2)
}, 0);
const intervalId = setInterval(function() {
//setinterval1
console.log(3)
}, 0)
setTimeout(function() {
//settimeout2
console.log(10)
new Promise(function(resolve) {
//promise1
console.log(11)
resolve()
})
.then(function() {
console.log(12)
})
.then(function() {
console.log(13)
clearInterval(intervalId)
})
}, 0);
//promise2
Promise.resolve()
.then(function() {
console.log(7)
})
.then(function() {
console.log(8)
})
console.log(9)
理解:
第一次事件循環(huán):
console.log(1)被執(zhí)行,輸出1
settimeout1執(zhí)行凡纳,加入macrotask隊(duì)列
setinterval1執(zhí)行窃植,加入macrotask隊(duì)列
settimeout2執(zhí)行,加入macrotask隊(duì)列
promise2執(zhí)行惫企,它的兩個(gè)then函數(shù)加入microtask隊(duì)列
console.log(9)執(zhí)行撕瞧,輸出9
關(guān)于Promise陵叽,Promise的回調(diào)函數(shù)不是傳入的,而是使用then來調(diào)用的丛版。因此巩掺,Promise中定義的函數(shù)應(yīng)該是馬上執(zhí)行的,then才是其回調(diào)函數(shù)页畦,放入microtask中胖替。
根據(jù)事件循環(huán)的定義,接下來會(huì)執(zhí)行新增的microtask任務(wù)豫缨,按照進(jìn)入隊(duì)列的順序独令,執(zhí)行console.log(7)和console.log(8),輸出7和8;
當(dāng)然這里肯定有一個(gè)疑問好芭,為什么不執(zhí)行macrotask隊(duì)列中的任務(wù)燃箭?這是因?yàn)橐婚_始js主線程中跑的任務(wù)就是macrotask任務(wù),而根據(jù)事件循環(huán)的流程舍败,一次事件循環(huán)只會(huì)執(zhí)行一個(gè)macrotask任務(wù)招狸,因此,執(zhí)行完主線程的代碼后邻薯,它就去從microtask隊(duì)列里取隊(duì)首任務(wù)來執(zhí)行裙戏。
microtask隊(duì)列為空,回到第一步厕诡,進(jìn)入下一個(gè)事件循環(huán)累榜,此時(shí)macrotask隊(duì)列為: settimeout1,setinterval1,settimeout2
第二次事件循環(huán):
從macrotask隊(duì)列里取位于隊(duì)首的任務(wù)(settimeout1)并執(zhí)行,輸出2
microtask隊(duì)列為空灵嫌,回到第一步壹罚,進(jìn)入下一個(gè)事件循環(huán),此時(shí)macrotask隊(duì)列為: setinterval1,settimeout2
第三次事件循環(huán):
從macrotask隊(duì)列里取位于隊(duì)首的任務(wù)(setinterval1)并執(zhí)行寿羞,輸出3,然后又將新生成的setinterval1加入macrotask隊(duì)列
microtask隊(duì)列為空渔嚷,回到第一步,進(jìn)入下一個(gè)事件循環(huán)稠曼,此時(shí)macrotask隊(duì)列為: settimeout2,setinterval1
第四次事件循環(huán):
從macrotask隊(duì)列里取位于隊(duì)首的任務(wù)(settimeout2)并執(zhí)行,輸出10,并且執(zhí)行new Promise內(nèi)的函數(shù)(new Promise內(nèi)的函數(shù)是同步操作客年,并不是異步操作),輸出11霞幅,并且將它的兩個(gè)then函數(shù)加入microtask隊(duì)列
從microtask隊(duì)列中,取隊(duì)首的任務(wù)執(zhí)行量瓜,直到為空為止司恳。因此,兩個(gè)新增的microtask任務(wù)按順序執(zhí)行绍傲,輸出12和13扔傅,并且將setinterval1清空
此時(shí)耍共,microtask隊(duì)列和macrotask隊(duì)列都為空,瀏覽器會(huì)一直檢查隊(duì)列是否為空猎塞,等待新的任務(wù)加入隊(duì)列试读。
當(dāng)然,