JavaScript執(zhí)行機(jī)制詳細(xì)介紹
前言:
不論是工作還是面試,我們可能都經(jīng)常會(huì)碰到需要知道代碼的執(zhí)行順序的場(chǎng)景,所以打算花點(diǎn)時(shí)間徹底搞懂JavaScript的執(zhí)行機(jī)制。
想要搞懂JavaScript執(zhí)行機(jī)制,你需要清楚下面這些知識(shí): (以瀏覽器環(huán)境為例,與Node環(huán)境不同)
1.進(jìn)程與線程的概念
- 瀏覽器原理
- 事件循環(huán)(Event-Loop),任務(wù)隊(duì)列(同步任務(wù),異步任務(wù),微任務(wù),宏任務(wù))
- 進(jìn)程與線程
我們都知道計(jì)算機(jī)的核心是CPU,它承擔(dān)了所有的計(jì)算任務(wù);而操作系統(tǒng)是計(jì)算機(jī)的管理者,它負(fù)責(zé)任務(wù)的調(diào)度、資源的分配和管理,統(tǒng)領(lǐng)整個(gè)計(jì)算機(jī)硬件;應(yīng)用程序則是具有某種功能的程序,程序是運(yùn)行于操作系統(tǒng)之上的。
進(jìn)程:
進(jìn)程是一個(gè)具有獨(dú)立功能的程序在一個(gè)數(shù)據(jù)集上的一次動(dòng)態(tài)執(zhí)行的過程,是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位,是應(yīng)用程序運(yùn)行的載體 進(jìn)程是能擁有資源和獨(dú)立運(yùn)行的最小單位,也是程序執(zhí)行的最小單位。
進(jìn)程具有的特征:
- 動(dòng)態(tài)性:進(jìn)程是程序的一次執(zhí)行過程,是臨時(shí)的,有生命期的,是動(dòng)態(tài)產(chǎn)生,動(dòng)態(tài)消亡的;
- 并發(fā)性:任何進(jìn)程都可以同其他進(jìn)程一起并發(fā)執(zhí)行;
- 獨(dú)立性:進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位;
- 結(jié)構(gòu)性:進(jìn)程由程序、數(shù)據(jù)和進(jìn)程控制塊三部分組成。
線程:
線程是程序執(zhí)行中一個(gè)單一的順序控制流程,是程序執(zhí)行流的最小單元,是處理器調(diào)度和分派的基本單位。一個(gè)進(jìn)程可以有一個(gè)或多個(gè)線程,各個(gè)線程之間共享程序的內(nèi)存空間(也就是所在進(jìn)程的內(nèi)存空間)。一個(gè)標(biāo)準(zhǔn)的線程由線程ID、當(dāng)前指令指針(PC)、寄存器和堆棧組成。而進(jìn)程由內(nèi)存空間(代碼、數(shù)據(jù)、進(jìn)程空間、打開的文件)和一個(gè)或多個(gè)線程組成。
進(jìn)程與線程的區(qū)別:
- 線程是程序執(zhí)行的最小單位,而進(jìn)程是操作系統(tǒng)分配資源的最小單位;
- 一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成,線程是一個(gè)進(jìn)程中代碼的不同執(zhí)行路線;
- 進(jìn)程之間相互獨(dú)立,但同一進(jìn)程下的各個(gè)線程之間共享程序的內(nèi)存空間(包括代碼段、數(shù)據(jù)集、堆等)及一些進(jìn)程級(jí)的資源(如打開文件和信號(hào)),進(jìn)程與進(jìn)程之間互不可見;
- 調(diào)度和切換:線程上下文切換比進(jìn)程上下文切換要快得多。
JS為什么是單線程?
JavaScript
從它誕生之初就是作為瀏覽器的腳本語言,主要用來處理用戶交互以及操作DOM,這就決定了它只能是單線程的,否則會(huì)帶來非常復(fù)雜的同步問題。
舉個(gè)例子: 如果JS是多線程的,其中一個(gè)線程要修改一個(gè)DOM元素,另外一個(gè)線程想要?jiǎng)h除這個(gè)DOM元素,這時(shí)候?yàn)g覽器就不知道該聽誰的。所以為了避免復(fù)雜性,從一誕生,JavaScript就被設(shè)計(jì)成單線程。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)
2.瀏覽器原理
作為前端工程師,瀏覽器想必都不陌生,并且瀏覽器是多進(jìn)程的。
瀏覽器組成部分:
- 用戶界面:包括地址欄,前進(jìn)/后退/刷新/書簽??等按鈕
- 瀏覽器引擎:在用戶界面和呈現(xiàn)引擎之間傳送指令
- 渲染引擎:用來繪制請(qǐng)求的內(nèi)容
- 網(wǎng)絡(luò):用來完成網(wǎng)絡(luò)調(diào)用,例如http請(qǐng)求,它具有平臺(tái)無關(guān)的接口,可以在不同平臺(tái)上工作
- JavaScript解釋器:用來解析執(zhí)行
JavaScript
代碼 - 用戶界面后端:用于繪制基本的窗口小部件,比如組合框和窗口,底層使用操作系統(tǒng)的用戶接口
- 數(shù)據(jù)存儲(chǔ):屬于持久層,瀏覽器在硬盤中保存類似
cookie
的各種數(shù)據(jù),HTML5定義了web database技術(shù),這是一種輕量級(jí)完整的客戶端存儲(chǔ)技術(shù)
??注意:與大多數(shù)瀏覽器不同的是,谷歌(Chrome)瀏覽器的每個(gè)標(biāo)簽頁都分別對(duì)應(yīng)一個(gè)呈現(xiàn)引擎實(shí)例。每個(gè)標(biāo)簽頁都是一個(gè)獨(dú)立的進(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)程:
- 負(fù)責(zé)管理第三方插件
GPU進(jìn)程:
- 負(fù)責(zé)3D繪制與硬件加速(最多一個(gè))
渲染進(jìn)程:
- 負(fù)責(zé)頁面文檔解析,執(zhí)行與渲染
渲染進(jìn)程包含哪些線程?
GUI渲染線程:
- 主要負(fù)責(zé)解析HTML,CSS,構(gòu)建DOM樹,布局,繪制等
- 該線程與JavaScript引擎線程互斥,當(dāng)執(zhí)行
JavaScript
引擎線程時(shí),GUI渲染線程會(huì)被掛起,當(dāng)任務(wù)隊(duì)列空閑時(shí),主線程才會(huì)執(zhí)行GUI渲染
JavaScript引擎線程:
- 主要負(fù)責(zé)處理
JavaScript
腳本,執(zhí)行代碼(如V8引擎) - 瀏覽器同時(shí)只能有一個(gè)JS引擎線程在運(yùn)行JS程序,即JS是單線程的
- JS引擎線程與GUI渲染線程是互斥的,所以JS引擎會(huì)阻塞頁面渲染
定時(shí)觸發(fā)器線程:
- 負(fù)責(zé)執(zhí)行定時(shí)器函數(shù)(
setTimeout,setInterval
) - 瀏覽器定時(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í)行),這個(gè)線程就是定時(shí)觸發(fā)器線程,也叫定時(shí)器線程
- W3C在HTML標(biāo)準(zhǔn)中規(guī)定,規(guī)定要求
setTimeout
中低于4ms的時(shí)間間隔算為4ms
事件觸發(fā)線程:
- 負(fù)責(zé)將準(zhǔn)備好的事件交給JS引擎線程執(zhí)行
- 當(dāng)事件被觸發(fā)時(shí),該線程會(huì)把對(duì)應(yīng)的事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎處理
異步請(qǐng)求線程:
- 在
XMLHttpRequest
連接后瀏覽器會(huì)開一個(gè)線程 - 檢測(cè)請(qǐng)求狀態(tài)變更時(shí),如果有對(duì)應(yīng)的回調(diào)函數(shù),異步請(qǐng)求線程就會(huì)產(chǎn)生狀態(tài)變更事件,并把對(duì)應(yīng)的回調(diào)函數(shù)放入隊(duì)列中等待JS引擎執(zhí)行
3.同步與異步
由于JavaScript
是單線程的,這就決定了它的任務(wù)不可能只有同步任務(wù),那些耗時(shí)很長(zhǎng)的任務(wù)如果也按同步任務(wù)執(zhí)行的話將會(huì)導(dǎo)致頁面阻塞,
所以JavaScript任務(wù)一般分為兩類:
同步任務(wù):
同步任務(wù)指的是,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);
異步任務(wù):
異步任務(wù)指的是,不進(jìn)入主線程、而進(jìn)入"任務(wù)隊(duì)列"(Event queue)的任務(wù),只有"任務(wù)隊(duì)列"通知主線程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行。
常見的異步任務(wù): 定時(shí)器,ajax,事件綁定,回調(diào)函數(shù),promise
,async await
等
- 同步和異步任務(wù)分別進(jìn)入不同的執(zhí)行"場(chǎng)所",同步的進(jìn)入主線程,異步的進(jìn)入Event Table并注冊(cè)函數(shù)。
- 當(dāng)Event Table中指定的事情完成時(shí),會(huì)將這個(gè)函數(shù)移入Event Queue。
- 主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會(huì)去
Event Queue
讀取對(duì)應(yīng)的函數(shù),進(jìn)入主線程執(zhí)行。 - 上述過程會(huì)不斷重復(fù),也就是常說的Event Loop(事件循環(huán))。
- 我們不禁要問了,那怎么知道主線程執(zhí)行棧為空???js引擎存在
monitoring process
進(jìn)程,會(huì)持續(xù)不斷的檢查主線程執(zhí)行棧是否為空,一旦為空,就會(huì)去Event Queue
那里檢查是否有等待被調(diào)用的函數(shù)。
宏任務(wù)與微任務(wù):
JavaScript除了廣義上的同步任務(wù)與異步任務(wù),還有更精細(xì)的任務(wù)定義:
- 宏任務(wù)(macro-task): 包括全局代碼,setTimeout,setInterval
- 微任務(wù)(micro-task): new Promise().then(回調(diào)) process.nextTick()
不同類型的任務(wù)會(huì)進(jìn)入到不同的任務(wù)隊(duì)列:
事件循環(huán)的順序,決定js代碼的執(zhí)行順序。進(jìn)入整體代碼(宏任務(wù))后,開始第一次循環(huán)。接著執(zhí)行所有的微任務(wù)。然后再次從宏任務(wù)開始,找到其中一個(gè)任務(wù)隊(duì)列執(zhí)行完畢,再執(zhí)行所有的微任務(wù)。
4.執(zhí)行棧與任務(wù)隊(duì)列
執(zhí)行棧:
JavaScript代碼都是在執(zhí)行上下文中執(zhí)行的,在JavaScript中有三種執(zhí)行上下文:
- 全局執(zhí)行上下文
- 函數(shù)執(zhí)行上下文,JS函數(shù)被調(diào)用時(shí)都會(huì)創(chuàng)建一個(gè)函數(shù)執(zhí)行上下文
- eval執(zhí)行上下文,eval函數(shù)產(chǎn)生的上下文(用的較少)
通常來說我們的JS代碼都不止一個(gè)上下文,那么這些上下文的執(zhí)行順序是怎樣的呢?
我們都知道棧是一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),我們JavaScript
中的執(zhí)行棧就是一種這樣的棧結(jié)構(gòu),當(dāng)JS引擎執(zhí)行代碼時(shí),會(huì)產(chǎn)生一個(gè)全局上下文并把它壓入執(zhí)行棧,每當(dāng)遇到函數(shù)調(diào)用時(shí),就會(huì)產(chǎn)生函數(shù)執(zhí)行上下文并壓入執(zhí)行棧。引擎從棧頂開始執(zhí)行函數(shù),執(zhí)行完后會(huì)彈出該執(zhí)行上下文。
function add(){ console.log(1) foo() console.log(3) } function foo(){ console.log(2) } add()
我們來看下上面這段代碼的執(zhí)行棧是怎樣的:
任務(wù)隊(duì)列:
前面我們說到了JavaScript
中所有的任務(wù)分為同步任務(wù)與異步任務(wù),同步任務(wù),顧名思義就是立即執(zhí)行的任務(wù),它一般是直接進(jìn)入到主線程中執(zhí)行。而我們的異步任務(wù)則是進(jìn)入任務(wù)隊(duì)列等待主線程中的任務(wù)執(zhí)行完再執(zhí)行。
任務(wù)隊(duì)列是一個(gè)事件的隊(duì)列,表示相關(guān)的異步任務(wù)可以進(jìn)入執(zhí)行棧了。主線程讀取任務(wù)隊(duì)列就是讀取里面有哪些事件。
隊(duì)列是一種先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)。
上面我們說到異步任務(wù)又可以分為宏任務(wù)與微任務(wù),所以任務(wù)隊(duì)列也可以分為宏任務(wù)隊(duì)列與微任務(wù)隊(duì)列
Macrotask Queue
:進(jìn)行比較大型的工作,常見的有setTimeout
,setInterval
,用戶交互操作,UI渲染等;Microtask Queue
:進(jìn)行較小的工作,常見的有Promise
,Process.nextTick
;
5.事件循環(huán)(Event-Loop)
- 同步任務(wù)直接放入到主線程執(zhí)行,異步任務(wù)(點(diǎn)擊事件,定時(shí)器,ajax等)掛在后臺(tái)執(zhí)行,等待I/O事件完成或行為事件被觸發(fā)。
- 系統(tǒng)后臺(tái)執(zhí)行異步任務(wù),如果某個(gè)異步任務(wù)事件(或者行為事件被觸發(fā)),則將該任務(wù)添加到任務(wù)隊(duì)列,并且每個(gè)任務(wù)會(huì)對(duì)應(yīng)一個(gè)回調(diào)函數(shù)進(jìn)行處理。
- 這里異步任務(wù)分為宏任務(wù)與微任務(wù),宏任務(wù)進(jìn)入到宏任務(wù)隊(duì)列,微任務(wù)進(jìn)入到微任務(wù)隊(duì)列。
- 執(zhí)行任務(wù)隊(duì)列中的任務(wù)具體是在執(zhí)行棧中完成的,當(dāng)主線程中的任務(wù)全部執(zhí)行完畢后,去讀取微任務(wù)隊(duì)列,如果有微任務(wù)就會(huì)全部執(zhí)行,然后再去讀取宏任務(wù)隊(duì)列
- 上述過程會(huì)不斷的重復(fù)進(jìn)行,也就是我們常說的事件循環(huán)(
Event-Loop
)。
例題驗(yàn)證:
我們來看道題目進(jìn)行驗(yàn)證
(async ()=>{ console.log(1) setTimeout(() => { console.log('setTimeout1') }, 0); function foo (){ return new Promise((res,rej) => { console.log(2) res(3) }) } new Promise((resolve,reject)=>{ console.log(4) resolve() console.log(5) }).then(()=> { console.log('6') }) const res = await foo(); console.log(res); console.log('7') setTimeout(_ => console.log('setTimeout2')) })()
打印順序是:1,4,5,2,6,3,7,setTimeout1,setTimeout2
分析:
- 代碼自上而下執(zhí)行,先遇到
console.log(1)
,直接打印1,接著遇到定時(shí)器屬于宏任務(wù),放入宏任務(wù)隊(duì)列 - 再遇到promise,由于
new Promise
是一個(gè)同步任務(wù),所以直接打印4,遇到resolve,也就是后面的then
函數(shù),放入微任務(wù)隊(duì)列,再打印5 - 然后再執(zhí)行await foo,foo函數(shù)里面有個(gè)
promise
,new promise
屬于同步任務(wù),所以會(huì)直接打印2,await返回的是一個(gè)promise
的回調(diào),await后面的任務(wù)放入微任務(wù)隊(duì)列 - 最后遇到一個(gè)定時(shí)器,放入宏任務(wù)隊(duì)列
- 執(zhí)行棧任務(wù)執(zhí)行完了,先去微任務(wù)隊(duì)列獲取微任務(wù)執(zhí)行,先執(zhí)行第一個(gè)微任務(wù),打印6,再執(zhí)行第二個(gè)微任務(wù),打印3,7
- 微任務(wù)執(zhí)行完,再去宏任務(wù)隊(duì)列獲取宏任務(wù)執(zhí)行,打印
setTimeout1
,setTimeout2
6.定時(shí)器
JavaScript
的任務(wù)隊(duì)列中的異步任務(wù)還包括定時(shí)器事件,即指定某些代碼在多長(zhǎng)時(shí)間后執(zhí)行。定時(shí)器功能主要由setTimeout()
和setInterval()
兩個(gè)函數(shù)來完成,他們的內(nèi)部執(zhí)行機(jī)制完全一樣,區(qū)別主要在于setTimeout
是一次執(zhí)行的過程,setInterval
則是反復(fù)執(zhí)行的過程。
setTimeout
函數(shù)接受兩個(gè)參數(shù),第一個(gè)是要執(zhí)行的回調(diào)函數(shù),第二個(gè)是推遲執(zhí)行的時(shí)間(ms)
如果我們把推遲時(shí)間設(shè)為0,是不是就會(huì)立即執(zhí)行呢?
setTimeout(()=>{ console.log(1) },0) console.log(2)
但事實(shí)并不是這樣的,上面的打印結(jié)果是先打印2,再打印1。是不是覺得很蒙?
我們用上面的事件循環(huán)的規(guī)則來理解就很清晰了,全局代碼執(zhí)行,遇到定時(shí)器setTimeout
,放入宏任務(wù)隊(duì)列,接著往下執(zhí)行同步代碼,打印2,執(zhí)行棧任務(wù)執(zhí)行完再去微任務(wù)隊(duì)列,沒有微任務(wù)再去看宏任務(wù)隊(duì)列,有一個(gè)宏任務(wù),執(zhí)行打印1。
setTimeout(fn,0)的含義是,指定某個(gè)任務(wù)在主線程最早可得的空閑時(shí)間執(zhí)行,也就是說,盡可能早得執(zhí)行。它在"任務(wù)隊(duì)列"的尾部添加一個(gè)事件,因此要等到同步任務(wù)和"任務(wù)隊(duì)列"現(xiàn)有的事件都處理完,才會(huì)得到執(zhí)行。
HTML5標(biāo)準(zhǔn)規(guī)定了setTimeout()的第二個(gè)參數(shù)的最小值(最短間隔),不得低于4毫秒,如果低于這個(gè)值,就會(huì)自動(dòng)增加。在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒。另外,對(duì)于那些DOM的變動(dòng)(尤其是涉及頁面重新渲染的部分),通常不會(huì)立即執(zhí)行,而是每16毫秒執(zhí)行一次。這時(shí)使用requestAnimationFrame()
的效果要好于setTimeout()。
需要注意的是,setTimeout()
只是將事件插入了"任務(wù)隊(duì)列",必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)。要是當(dāng)前代碼耗時(shí)很長(zhǎng),有可能要等很久,所以并沒有辦法保證,回調(diào)函數(shù)一定會(huì)在setTimeout()
指定的時(shí)間執(zhí)行。
到此這篇關(guān)于JavaScript執(zhí)行機(jī)制詳細(xì)介紹的文章就介紹到這了,更多相關(guān)JavaScript執(zhí)行機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript日拱算法題解滑動(dòng)窗口的最大值示例
這篇文章主要為大家介紹了JavaScript日拱算法題解滑動(dòng)窗口的最大值實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10四十九個(gè)javascript小知識(shí)實(shí)用技巧
這篇文章主要給大家分享得是四十九個(gè)javascript小知識(shí)實(shí)用技巧像下面文章圍繞JavaScript得各種技巧詳細(xì)介紹,需要的朋友可以參考一下,希望對(duì)你有所幫助2021-11-11微信小程序 仿貓眼實(shí)現(xiàn)實(shí)例代碼
這篇文章主要介紹了微信小程序 仿貓眼實(shí)現(xiàn)實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03自行實(shí)現(xiàn)Promise.allSettled的Polyfill處理
這篇文章主要為大家介紹了自行實(shí)現(xiàn)Promise.allSettled?的?Polyfill處理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08微信小程序 switch組件詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了微信小程序 switch組件詳解及簡(jiǎn)單實(shí)例 的相關(guān)資料,需要的朋友可以參考下2017-01-01微信小程序 高德地圖SDK詳解及簡(jiǎn)單實(shí)例(源碼下載)
這篇文章主要介紹了微信小程序 高德地圖詳解及簡(jiǎn)單實(shí)例(源碼下載)的相關(guān)資料,需要的朋友可以參考下2017-01-01