簡單聊聊JavaScript中的事件循環(huán)
為什么js是單線程的
我們首先要考慮下js作為瀏覽器腳本語言,主要用途就是和用戶互動(dòng)和操作DOM。比如js同時(shí)有兩個(gè)線程,兩個(gè)線程同時(shí)操作同一個(gè)DOM,比如一個(gè)給DOM添加內(nèi)容,一個(gè)移除DOM,那到底該聽誰的呢?所以這就決定了它只能是單線程,否則就會(huì)出現(xiàn)一些奇怪的問題。
瀏覽器
我們每打開一個(gè)tab頁就會(huì)產(chǎn)生一個(gè)進(jìn)程
瀏覽器都包含哪些進(jìn)程呢
瀏覽器進(jìn)程
- 瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)、主控),該進(jìn)程只有一個(gè)
- 負(fù)責(zé)瀏覽器界面顯示,用戶交互。如前進(jìn),后退等
- 負(fù)責(zé)各個(gè)頁面的管理,創(chuàng)建和銷毀其他進(jìn)程
- 將渲染(renderer)進(jìn)程得到的內(nèi)存中的Bitmap(位圖),繪制到用戶界面上
- 網(wǎng)絡(luò)資源的管理,下載等
第三方插件進(jìn)程
每種類型的插件對應(yīng)一個(gè)進(jìn)程,當(dāng)使用該插件時(shí)才創(chuàng)建。因插件易崩潰,所以需要通過插件進(jìn)程來隔離,以保證插件進(jìn)程崩潰不會(huì)對瀏覽器和頁面造成影響
GPU進(jìn)程
該進(jìn)程只有一個(gè),用于3D繪制等
渲染進(jìn)程
- 通常所說的瀏覽器內(nèi)核(Renderer進(jìn)程,內(nèi)部是多線程)
- 每個(gè)Tab頁面都有一個(gè)渲染進(jìn)程,互不影響
- 主要作用為頁面渲染,腳本執(zhí)行,事件處理等
網(wǎng)絡(luò)進(jìn)程
主要負(fù)責(zé)頁面的網(wǎng)絡(luò)資源加載。
如果瀏覽器是單進(jìn)程,那么當(dāng)一個(gè)tab頁面崩潰了,就會(huì)影響到整個(gè)瀏覽器。同時(shí)如果插件崩潰了也會(huì)影響整個(gè)瀏覽器。瀏覽器進(jìn)程有很多,每個(gè)進(jìn)程又有很多的線程,都會(huì)占用內(nèi)存。進(jìn)程之間的內(nèi)容相互隔離,這是為了保護(hù)操作系統(tǒng)中進(jìn)行互不干擾的技術(shù),每一個(gè)進(jìn)程只能訪問自己占有的數(shù)據(jù),也就避免了進(jìn)程A寫入數(shù)據(jù)到進(jìn)程B的情況。因?yàn)檫M(jìn)程之間的數(shù)據(jù)是嚴(yán)格隔離的,所以一個(gè)進(jìn)程如果崩潰了,或者掛起了,是不會(huì)影響到其他進(jìn)程的。
渲染進(jìn)程
頁面的渲染、js的執(zhí)行、事件的循環(huán)、都在渲染進(jìn)程中執(zhí)行,所以重點(diǎn)看下渲染進(jìn)程。渲染進(jìn)程是多線程的,下面看下比較常用的幾個(gè)線程
GUI線程
負(fù)責(zé)渲染瀏覽器界面,解析HTML,CSS,構(gòu)建DOM樹和RenderObject樹,布局和繪制等。
當(dāng)修改了一些元素的顏色或者背景色,頁面就會(huì)重繪(Repaint)
當(dāng)修改元素的尺寸,頁面就是重排也叫回流(Reflow)
當(dāng)頁面需要重繪和重排的時(shí)候GUI線程執(zhí)行,繪制頁面
重繪和重排的成本比較高,盡量避免重繪和重排
GUI線程和JS引擎線程是互斥的
- 當(dāng)JS引擎執(zhí)行時(shí),GUI線程會(huì)被掛起
- GUI更新會(huì)被保存在一個(gè)隊(duì)列中,等JS引擎空閑的時(shí)候立即被執(zhí)行。
JS引擎線程
JS引擎線程就是JS內(nèi)核,負(fù)責(zé)處理JavaScript腳本程序(例如V8引擎)
JS引擎線程負(fù)責(zé)解析JavaScript腳本,運(yùn)行代碼
JS引擎一直等待任務(wù)隊(duì)列的到來,然后加以處理
- 瀏覽器同時(shí)只能有一個(gè)JS引擎線程在運(yùn)行JS程序,所以JS是單線程運(yùn)行的
- 一個(gè)Tab頁在Renderer進(jìn)程中無論什么時(shí)候都只有一個(gè)JS線程在運(yùn)行JS程序
GUI線程和JS引擎是互斥的,JS引擎線程會(huì)阻塞GUI渲染線程
如果JS執(zhí)行的時(shí)間過長,這樣就會(huì)造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞。
事件觸發(fā)線程
- 屬于瀏覽器而不是JS引擎,用來控制事件循環(huán),并且管理著一個(gè)事件隊(duì)列(task queue)
- 當(dāng)JS引擎執(zhí)行事件綁定和一些異步操作如SetTimeOut時(shí),也可能是瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊、Ajax異步請求等,會(huì)走事件觸發(fā)線程將對應(yīng)的事件添加到對應(yīng)的線程中(比如定時(shí)器操作,便把定時(shí)器事件添加到定時(shí)器線程),等異步事件有了結(jié)果,便把他們的回調(diào)操作添加到事件隊(duì)列,等待js引擎線程空閑時(shí)來處理。
- 當(dāng)對應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎的處理
- JS是單線程,所以這些待處理隊(duì)列中的事件都會(huì)排隊(duì)等待JS引擎處理
定時(shí)觸發(fā)器線程
- setInterval與setTimeout所在線程
- 瀏覽器定時(shí)計(jì)數(shù)器并不是由JS引擎計(jì)數(shù)的(因?yàn)镴S引擎是單線程的,如果處于阻塞線程狀態(tài)就會(huì)影響計(jì)時(shí)的準(zhǔn)確性)
- 通過單獨(dú)線程來計(jì)時(shí)并觸發(fā)定時(shí)(計(jì)時(shí)完畢后,添加到事件觸發(fā)線程的事件隊(duì)列中,等待JS引擎空閑后執(zhí)行)
- 注意,W3C在HTML標(biāo)準(zhǔn)中規(guī)定,規(guī)定要求setTimeout中低于4ms的時(shí)間間隔算為4ms。
異步HTTP請求線程
- 在XMLHttpRequest在連接后是通過瀏覽器新開的一個(gè)線程請求
- 將檢測到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù),異步線程就會(huì)產(chǎn)生狀態(tài)變更事件,將這個(gè)回調(diào)再放入事件隊(duì)列中再由JS引擎執(zhí)行
- 簡單說就是當(dāng)執(zhí)行到一個(gè)http異步請求時(shí),就把異步請求事件添加到異步請求線程,等收到響應(yīng)(準(zhǔn)確來說應(yīng)該是http狀態(tài)變化),再把回調(diào)函數(shù)添加到事件隊(duì)列,等待js引擎線程來執(zhí)行
下面就來談?wù)勎覀兊闹仡^戲
事件循環(huán)
- JS被分為同步任務(wù)和異步任務(wù)。
- 同步任務(wù)在主線程(JS引擎線程)上執(zhí)行,形成一個(gè)執(zhí)行棧。
- 除了主線程之外,事件觸發(fā)線程管理這一個(gè)任務(wù)隊(duì)列,只要異步任務(wù)有了結(jié)果,就會(huì)在任務(wù)隊(duì)列中放入異步任務(wù)的回調(diào)。
- 當(dāng)執(zhí)行棧中所有的同步任務(wù)執(zhí)行完畢后,就會(huì)讀取任務(wù)隊(duì)列,將可運(yùn)行的異步任務(wù)(任務(wù)隊(duì)列中的事件回調(diào),只要任務(wù)隊(duì)列中有事件回調(diào),就說明可以執(zhí)行)添加到執(zhí)行棧中,開始執(zhí)行。 我們畫個(gè)圖來表示一下
let setTimeoutCallBack = function() { console.log('我是定時(shí)器回調(diào)'); }; let httpCallback = function() { console.log('我是http請求回調(diào)'); } // 同步任務(wù) console.log('我是同步任務(wù)1'); // 異步定時(shí)任務(wù) setTimeout(setTimeoutCallBack,1000); // 異步http請求任務(wù) ajax.get('/info',httpCallback); // 同步任務(wù) console.log('我是同步任務(wù)2');
我們來看下這段代碼。解析一下執(zhí)行過程
- js會(huì)從上到下依次執(zhí)行,可以先理解為這段代碼的執(zhí)行環(huán)境就是主線程,也就是當(dāng)前執(zhí)行棧
- 首先 執(zhí)行
console.log('我是同步任務(wù)1');
- 然后執(zhí)行到
setTimeout
時(shí)候,會(huì)交給定時(shí)器線程,并告訴定時(shí)器線程在1s后將setTimeoutCallBack
回調(diào)交給事件觸發(fā)線程,1s后事件觸發(fā)線程把這個(gè)回調(diào)添加到了任務(wù)隊(duì)列中等待執(zhí)行 - 接著執(zhí)行
ajax
,會(huì)交給異步HTTP請求線程發(fā)送網(wǎng)絡(luò)請求,請求成功后,將回調(diào)httpCallback
交給事件觸發(fā)線程并放入任務(wù)隊(duì)列中等待執(zhí)行。 - 接著執(zhí)行
console.log('我是同步任務(wù)2');
- 此時(shí)主線程執(zhí)行棧執(zhí)行完畢,js引擎線程已經(jīng)空閑,開始詢問事件觸發(fā)線程的任務(wù)隊(duì)列中是否有需要執(zhí)行的回調(diào),如果有則將任務(wù)隊(duì)列中的回調(diào)事件加入執(zhí)行棧中,開始執(zhí)行,如果任務(wù)隊(duì)列中沒有需要執(zhí)行的回調(diào),js引擎會(huì)不斷的發(fā)起詢問,直到有為止。
瀏覽器上的所有線程是的行為都很單一。
- 定時(shí)觸發(fā)線程只管理定時(shí)器并只關(guān)心定時(shí)不關(guān)注結(jié)果,定時(shí)結(jié)束后就把回調(diào)交給事件觸發(fā)線程
- 異步HTTP請求線程只關(guān)心http請求不關(guān)心結(jié)果,請求結(jié)束后就把回調(diào)交給時(shí)間觸發(fā)線程
- 事件觸發(fā)線程只將異步回調(diào)加入事件隊(duì)列
- JS引擎線程則執(zhí)行執(zhí)行棧中的事件,執(zhí)行棧中的代碼執(zhí)行完畢后,就會(huì)詢問事件觸發(fā)線程的事件隊(duì)列是否有回調(diào)需要執(zhí)行,然后把事件隊(duì)列中的事件添加到執(zhí)行棧中執(zhí)行,這樣的反反復(fù)復(fù)的行為我們稱為事件循環(huán)。
了解了事件循環(huán)下面看下宏任務(wù)和微任務(wù)
宏任務(wù) 微任務(wù)
宏任務(wù)
- 渲染事件(如解析 DOM、計(jì)算布局、繪制)
- 用戶交互事件(如鼠標(biāo)點(diǎn)擊、滾動(dòng)頁面、放大縮小等)
- JavaScript 腳本執(zhí)行事件
- 網(wǎng)絡(luò)請求完成、文件讀寫完成事件
為了協(xié)調(diào)這些任務(wù)能夠有序的在主線程上執(zhí)行,頁面進(jìn)行引入了消息隊(duì)列和事件循環(huán)機(jī)制,渲染進(jìn)程內(nèi)部會(huì)維護(hù)多個(gè)消息隊(duì)列,主線程不斷的從這些任務(wù)隊(duì)列中取出任務(wù)并執(zhí)行任務(wù)。我們把這些消息隊(duì)列中的任務(wù)稱為宏任務(wù)
常見的宏任務(wù):
- 主代碼塊
- setTimeOut
- setInterval
- setImmediate -- node
- requestAnimationFrame -- 瀏覽器 JS引擎線程和GUI渲染線程是互斥的,瀏覽器為了能夠使宏任務(wù)和DOM任務(wù)有序的進(jìn)行,會(huì)在一個(gè)宏任務(wù)結(jié)束后,在一個(gè)宏任務(wù)執(zhí)行前,GUI渲染線程開始工作,對頁面進(jìn)行渲染
微任務(wù)
微任務(wù)就是一個(gè)需要異步執(zhí)行的函數(shù),執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束后之前。
異步回調(diào)有兩種方式
- 把異步回調(diào)函數(shù)封裝成一個(gè)宏任務(wù),添加到消息隊(duì)列中,當(dāng)循環(huán)系統(tǒng)執(zhí)行到該任務(wù)的時(shí)候執(zhí)行回調(diào)函數(shù)
- 執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束之前執(zhí)行回調(diào)函數(shù),這通常都是以微任務(wù)的形式體現(xiàn)的
我們知道宏任務(wù)結(jié)束后,會(huì)執(zhí)行渲染,然后執(zhí)行下一次宏任務(wù),微任務(wù)可以理解為當(dāng)前宏任務(wù)執(zhí)行后立即執(zhí)行的任務(wù)。
常見的微任務(wù):
- process.nextTick()--node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
當(dāng)執(zhí)行完一個(gè)宏任務(wù)之后,會(huì)立即執(zhí)行期間所產(chǎn)生的所有微任務(wù),然后執(zhí)行渲染
宏任務(wù)微任務(wù)的執(zhí)行流程
瀏覽器首先會(huì)執(zhí)行一個(gè)宏任務(wù),然后執(zhí)行當(dāng)前執(zhí)行棧所產(chǎn)生的微任務(wù),然后再渲染頁面,然后再執(zhí)行下一個(gè)宏任務(wù)
面試題
function test() { console.log(1) setTimeout(function () { // timer1 console.log(2) }, 1000) } test(); setTimeout(function () { // timer2 console.log(3) }) new Promise(function (resolve) { console.log(4) setTimeout(function () { // timer3 console.log(5) }, 100) resolve() }).then(function () { setTimeout(function () { // timer4 console.log(6) }, 0) console.log(7) }) console.log(8)
下面我們來分析一下整體的流程
首先應(yīng)該找到同步任務(wù)先執(zhí)行
- 當(dāng)test()調(diào)用的時(shí)候
console.log(1)
會(huì)先執(zhí)行,打印1。而setTimeout(我們記作timer1)作為宏任務(wù)加入宏任務(wù)隊(duì)列 - test下面的setTimtout(我們記作timer2)作為宏任務(wù)加入宏任務(wù)隊(duì)列
- new Promise()的executer中中也會(huì)當(dāng)做同步任務(wù)執(zhí)行 所以
console.log(4)
打印4。而setTimeout(我們記作timer3)作為宏任務(wù)加入宏任務(wù)隊(duì)列 - 接著promise.then()作為微任務(wù)加入微任務(wù)隊(duì)列
- 最后
console.log(8)
作為同步任務(wù)執(zhí)行,打印8
我們再看異步任務(wù)
- 我們當(dāng)前的執(zhí)行棧本身就是宏任務(wù),宏任務(wù)執(zhí)行完了之后應(yīng)該立即執(zhí)行微任務(wù),這里的微任務(wù)只有Promise.then(),而setTimeout(我們記作timer4)作為宏任務(wù)加入宏任務(wù)隊(duì)列,然后執(zhí)行
console.log(7)
打印7 - 微任務(wù)執(zhí)行完畢之后,要執(zhí)行GUI渲染,我們代碼中沒有
- 執(zhí)行宏任務(wù)隊(duì)列,此時(shí)宏任務(wù)隊(duì)列里面有 timer1、timer2、timer3、timer4
- 按照定時(shí)時(shí)間,可以排列為:timer2、timer4、timer3、timer1依次拿出放入執(zhí)行棧末尾執(zhí)行
- 執(zhí)行timer2,
console.log(3)
作為同步任務(wù)打印3,然后檢查有沒有微任務(wù)和GUI渲染 - 執(zhí)行timer4,
console.log(6)
作為同步任務(wù)打印6,然后檢查有沒有微任務(wù)和GUI渲染 - 執(zhí)行timer3,
console.log(5)
作為同步任務(wù)打印5,然后檢查有沒有微任務(wù)和GUI渲染 - 執(zhí)行timer1,
console.log(2)
作為同步任務(wù)打印2,然后檢查有沒有微任務(wù)和GUI渲染 所以最終結(jié)果為:1、4、8、7、3、6、5、2
到此這篇關(guān)于簡單聊聊JavaScript中的事件循環(huán)的文章就介紹到這了,更多相關(guān)JavaScript事件循環(huán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章

JavaScript實(shí)現(xiàn)網(wǎng)頁下拉菜單效果

TypeScript中Array(數(shù)組)聲明與簡單使用方法

JavaScript超詳細(xì)實(shí)現(xiàn)網(wǎng)頁輪播圖

JS實(shí)現(xiàn)轉(zhuǎn)動(dòng)隨機(jī)數(shù)抽獎(jiǎng)特效代碼

javascript smipleChart 簡單圖標(biāo)類

JavaScript中三個(gè)等號和兩個(gè)等號的區(qū)別(== 和 ===)淺析