在之前的博客中,我們認(rèn)識(shí)了瀏覽器是如何渲染頁面的?。今天來學(xué)習(xí)JavaScript在瀏覽器中的運(yùn)行機(jī)制。
瀏覽器的渲染進(jìn)程是多線程的
- GUI渲染進(jìn)程
- 負(fù)責(zé)渲染瀏覽器界面,解析HTML,CSS,構(gòu)建DOM樹和RenderObject樹,布局和繪制等
- 當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行
- GUI渲染線程與JS引擎線程是互斥的,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起,GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行。
- JS引擎線程
- JS引擎線程負(fù)責(zé)解析Javascript腳本,運(yùn)行代碼。
- JS是單線程的
- GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時(shí)間過長,這樣就會(huì)造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞。
- 事件觸發(fā)線程
- 歸屬于瀏覽器而不是JS引擎,用來控制事件循環(huán)
- 當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(shí)(也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊、AJAX異步請(qǐng)求等),會(huì)將對(duì)應(yīng)任務(wù)添加到事件線程中
- 當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎的處理
- 由于JS的單線程關(guān)系,所以這些待處理隊(duì)列中的事件都得排隊(duì)等待JS引擎處理
JavaScript是單線程的
單線程模型指的是,JavaScript 只在一個(gè)線程上運(yùn)行。也就是說,JavaScript 同時(shí)只能執(zhí)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待。
這種模式的好處是實(shí)現(xiàn)起來比較簡單,執(zhí)行環(huán)境相對(duì)單純;壞處是只要有一個(gè)任務(wù)耗時(shí)很長,后面的任務(wù)都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行。比如等待 Ajax 請(qǐng)求返回結(jié)果。這個(gè)時(shí)候,如果對(duì)方服務(wù)器遲遲沒有響應(yīng),或者網(wǎng)絡(luò)不通暢,就會(huì)導(dǎo)致腳本的長時(shí)間停滯。
JavaScript 內(nèi)部采用的“事件循環(huán)”機(jī)制(Event Loop)。這時(shí) CPU 完全可以不管 IO 操作,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到 IO 操作返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。這種機(jī)制就是 JavaScript 內(nèi)部采用的“事件循環(huán)”機(jī)制(Event Loop)
任務(wù)隊(duì)列和事件循環(huán)
JavaScript 運(yùn)行時(shí),除了一個(gè)正在運(yùn)行的主線程,引擎還提供一個(gè)任務(wù)隊(duì)列(task queue),里面是各種需要當(dāng)前程序處理的異步任務(wù)。
首先,主線程會(huì)去執(zhí)行所有的同步任務(wù)。等到同步任務(wù)全部執(zhí)行完,就會(huì)去看任務(wù)隊(duì)列里面的異步任務(wù)。如果滿足條件,那么異步任務(wù)就重新進(jìn)入主線程開始執(zhí)行,這時(shí)它就變成同步任務(wù)了。等到執(zhí)行完,下一個(gè)異步任務(wù)再進(jìn)入主線程開始執(zhí)行。一旦任務(wù)隊(duì)列清空,程序就結(jié)束執(zhí)行。
異步任務(wù)的寫法通常是回調(diào)函數(shù)。一旦異步任務(wù)重新進(jìn)入主線程,就會(huì)執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。如果一個(gè)異步任務(wù)沒有回調(diào)函數(shù),就不會(huì)進(jìn)入任務(wù)隊(duì)列,也就是說,不會(huì)重新進(jìn)入主線程,因?yàn)闆]有用回調(diào)函數(shù)指定下一步的操作。
JavaScript 引擎怎么知道異步任務(wù)有沒有結(jié)果,能不能進(jìn)入主線程呢?答案就是引擎在不停地檢查,一遍又一遍,只要同步任務(wù)執(zhí)行完了,引擎就會(huì)去檢查那些掛起來的異步任務(wù),是不是可以進(jìn)入主線程了。這種循環(huán)檢查的機(jī)制,就叫做事件循環(huán)(Event Loop)。
異步操作的模式
- 回調(diào)函數(shù)
下面是兩個(gè)函數(shù)f1和f2,編程的意圖是f2必須等到f1執(zhí)行完成,才能執(zhí)行。
function f1(){
//...
}
function f2(){
//...
}
f1();
f2();
但是如果f1是異步操作,f2會(huì)立即執(zhí)行,不會(huì)等到f1結(jié)束再執(zhí)行。為了達(dá)到同一目的,我們可以用回調(diào)函數(shù)改寫
function f1(callback){
//...
callback();
}
function f2(){
//...
}
f1(f2)
回調(diào)函數(shù)的優(yōu)點(diǎn)是簡單、容易理解和實(shí)現(xiàn),缺點(diǎn)是不利于代碼的閱讀和維護(hù),各個(gè)部分之間高度耦合,使得程序結(jié)構(gòu)混亂、流程難以追蹤(尤其是多個(gè)回調(diào)函數(shù)嵌套的情況),而且每個(gè)任務(wù)只能指定一個(gè)回調(diào)函數(shù)。
- 事件監(jiān)聽
另一種思路是采用事件驅(qū)動(dòng)模式。異步任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個(gè)事件是否發(fā)生。
還是以f1和f2為例。首先,為f1綁定一個(gè)事件(這里采用的 jQuery 的寫法)
f1.on('done', f2);
上面這行代碼的意思是,當(dāng)f1發(fā)生done事件,就執(zhí)行f2。然后,對(duì)f1進(jìn)行改寫:
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
上面代碼中,f1.trigger('done')表示,執(zhí)行完成后,立即觸發(fā)done事件,從而開始執(zhí)行f2。
這種方法的優(yōu)點(diǎn)是比較容易理解,可以綁定多個(gè)事件,每個(gè)事件可以指定多個(gè)回調(diào)函數(shù),而且可以去耦合,有利于實(shí)現(xiàn)模塊化。缺點(diǎn)是整個(gè)程序都要變成事件驅(qū)動(dòng)型,運(yùn)行流程會(huì)變得很不清晰。閱讀代碼的時(shí)候,很難看出主流程。
定時(shí)器的運(yùn)行機(jī)制
setTimeout和setInterval的運(yùn)行機(jī)制,是將指定的代碼移出本輪事件循環(huán),等到下一輪事件循環(huán),再檢查是否到了指定時(shí)間。如果到了,就執(zhí)行對(duì)應(yīng)的代碼;如果不到,就繼續(xù)等待。
這意味著,setTimeout和setInterval指定的回調(diào)函數(shù),必須等到本輪事件循環(huán)的所有同步任務(wù)都執(zhí)行完,才會(huì)開始執(zhí)行。由于前面的任務(wù)到底需要多少時(shí)間執(zhí)行完,是不確定的,所以沒有辦法保證,setTimeout和setInterval指定的任務(wù),一定會(huì)按照預(yù)定時(shí)間執(zhí)行。