javascript
JS引擎线程的执行过程的三个阶段
瀏覽器首先按順序加載由<script>標(biāo)簽分割的js代碼塊,加載js代碼塊完畢后,立刻進(jìn)入以下三個(gè)階段,然后再按順序查找下一個(gè)代碼塊,再繼續(xù)執(zhí)行以下三個(gè)階段,無(wú)論是外部腳本文件(不異步加載)還是內(nèi)部腳本代碼塊,都是一樣的原理,并且都在同一個(gè)全局作用域中。
JS引擎線程的執(zhí)行過程的三個(gè)階段:
- 語(yǔ)法分析
- 預(yù)編譯階段
- 執(zhí)行階段
一. 語(yǔ)法分析
分析該js腳本代碼塊的語(yǔ)法是否正確,如果出現(xiàn)不正確,則向外拋出一個(gè)語(yǔ)法錯(cuò)誤(SyntaxError),停止該js代碼塊的執(zhí)行,然后繼續(xù)查找并加載下一個(gè)代碼塊;如果語(yǔ)法正確,則進(jìn)入預(yù)編譯階段。
下面階段的代碼執(zhí)行不會(huì)再進(jìn)行語(yǔ)法校驗(yàn),語(yǔ)法分析在代碼塊加載完畢時(shí)統(tǒng)一檢驗(yàn)語(yǔ)法。
二. 預(yù)編譯階段
1. js的運(yùn)行環(huán)境
全局環(huán)境(JS代碼加載完畢后,進(jìn)入代碼預(yù)編譯即進(jìn)入全局環(huán)境)
函數(shù)環(huán)境(函數(shù)調(diào)用執(zhí)行時(shí),進(jìn)入該函數(shù)環(huán)境,不同的函數(shù)則函數(shù)環(huán)境不同)
eval(不建議使用,會(huì)有安全,性能等問題)
每進(jìn)入一個(gè)不同的運(yùn)行環(huán)境都會(huì)創(chuàng)建一個(gè)相應(yīng)的執(zhí)行上下文(Execution Context),那么在一段JS程序中一般都會(huì)創(chuàng)建多個(gè)執(zhí)行上下文,js引擎會(huì)以棧的方式對(duì)這些執(zhí)行上下文進(jìn)行處理,形成函數(shù)調(diào)用棧(call stack),棧底永遠(yuǎn)是全局執(zhí)行上下文(Global Execution Context),棧頂則永遠(yuǎn)是當(dāng)前執(zhí)行上下文。
2. 函數(shù)調(diào)用棧/執(zhí)行棧
調(diào)用棧,也叫執(zhí)行棧,具有LIFO(后進(jìn)先出)結(jié)構(gòu),用于存儲(chǔ)在代碼執(zhí)行期間創(chuàng)建的所有執(zhí)行上下文。
首次運(yùn)行JS代碼時(shí),會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并Push到當(dāng)前的執(zhí)行棧中。每當(dāng)發(fā)生函數(shù)調(diào)用,引擎都會(huì)為該函數(shù)創(chuàng)建一個(gè)新的函數(shù)執(zhí)行上下文并Push到當(dāng)前執(zhí)行棧的棧頂。
當(dāng)棧頂函數(shù)運(yùn)行完成后,其對(duì)應(yīng)的函數(shù)執(zhí)行上下文將會(huì)從執(zhí)行棧中Pop出,上下文控制權(quán)將移到當(dāng)前執(zhí)行棧的下一個(gè)執(zhí)行上下文。
var a = 'Hello World!';function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); }function second() { console.log('Inside second function'); }first(); console.log('Inside Global Execution Context');// Inside first function // Inside second function // Again inside first function // Inside Global Execution Context復(fù)制代碼3. 執(zhí)行上下文的創(chuàng)建
執(zhí)行上下文可理解為當(dāng)前的執(zhí)行環(huán)境,與該運(yùn)行環(huán)境相對(duì)應(yīng),具體分類如上面所說(shuō)分為全局執(zhí)行上下文和函數(shù)執(zhí)行上下文。創(chuàng)建執(zhí)行上下文的三部曲:
創(chuàng)建變量對(duì)象(Variable Object)
建立作用域鏈(Scope Chain)
確定this的指向
3.1 創(chuàng)建變量對(duì)象
創(chuàng)建arguments對(duì)象:檢查當(dāng)前上下文中的參數(shù),建立該對(duì)象的屬性與屬性值,僅在函數(shù)環(huán)境(非箭頭函數(shù))中進(jìn)行,全局環(huán)境沒有此過程
檢查當(dāng)前上下文的函數(shù)聲明:按代碼順序查找,將找到的函數(shù)提前聲明,如果當(dāng)前上下文的變量對(duì)象沒有該函數(shù)名屬性,則在該變量對(duì)象以函數(shù)名建立一個(gè)屬性,屬性值則為指向該函數(shù)所在堆內(nèi)存地址的引用,如果存在,則會(huì)被新的引用覆蓋。
檢查當(dāng)前上下文的變量聲明:按代碼順序查找,將找到的變量提前聲明,如果當(dāng)前上下文的變量對(duì)象沒有該變量名屬性,則在該變量對(duì)象以變量名建立一個(gè)屬性,屬性值為undefined;如果存在,則忽略該變量聲明
函數(shù)聲明提前和變量聲明提升是在創(chuàng)建變量對(duì)象中進(jìn)行的,且函數(shù)聲明優(yōu)先級(jí)高于變量聲明。具體是如何函數(shù)和變量聲明提前的可以看后面。
創(chuàng)建變量對(duì)象發(fā)生在預(yù)編譯階段,但尚未進(jìn)入執(zhí)行階段,該變量對(duì)象都是不能訪問的,因?yàn)榇藭r(shí)的變量對(duì)象中的變量屬性尚未賦值,值仍為undefined,只有進(jìn)入執(zhí)行階段,變量對(duì)象中的變量屬性進(jìn)行賦值后,變量對(duì)象(Variable Object)轉(zhuǎn)為活動(dòng)對(duì)象(Active Object)后,才能進(jìn)行訪問,這個(gè)過程就是VO –> AO過程。
3.2 建立作用域鏈
通俗理解,作用域鏈由當(dāng)前執(zhí)行環(huán)境的變量對(duì)象(未進(jìn)入執(zhí)行階段前)與上層環(huán)境的一系列活動(dòng)對(duì)象組成,它保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問權(quán)限的變量和函數(shù)的有序訪問。
可以通過一個(gè)例子簡(jiǎn)單理解:
var num = 30;function test() {var a = 10;function innerTest() {var b = 20;return a + b}innerTest() }test()復(fù)制代碼在上面的例子中,當(dāng)執(zhí)行到調(diào)用innerTest函數(shù),進(jìn)入innerTest函數(shù)環(huán)境。全局執(zhí)行上下文和test函數(shù)執(zhí)行上下文已進(jìn)入執(zhí)行階段,innerTest函數(shù)執(zhí)行上下文在預(yù)編譯階段創(chuàng)建變量對(duì)象,所以他們的活動(dòng)對(duì)象和變量對(duì)象分別是AO(global),AO(test)和VO(innerTest),而innerTest的作用域鏈由當(dāng)前執(zhí)行環(huán)境的變量對(duì)象(未進(jìn)入執(zhí)行階段前)與上層環(huán)境的一系列活動(dòng)對(duì)象組成,如下:
innerTestEC = {//變量對(duì)象VO: {b: undefined}, //作用域鏈scopeChain: [VO(innerTest), AO(test), AO(global)], //this指向this: window }復(fù)制代碼深入理解的話,創(chuàng)建作用域鏈,也就是創(chuàng)建詞法環(huán)境,而詞法環(huán)境有兩個(gè)組成部分:
- 環(huán)境記錄:存儲(chǔ)變量和函數(shù)聲明的實(shí)際位置
- 對(duì)外部環(huán)境的引用:可以訪問其外部詞法環(huán)境
詞法環(huán)境類型偽代碼如下:
// 第一種類型: 全局環(huán)境 GlobalExectionContext = { // 全局執(zhí)行上下文LexicalEnvironment: { // 詞法環(huán)境EnvironmentRecord: { // 環(huán)境記錄Type: "Object", // 全局環(huán)境// 標(biāo)識(shí)符綁定在這里 outer: <null> // 對(duì)外部環(huán)境的引用} }// 第二種類型: 函數(shù)環(huán)境 FunctionExectionContext = { // 函數(shù)執(zhí)行上下文LexicalEnvironment: { // 詞法環(huán)境EnvironmentRecord: { // 環(huán)境記錄Type: "Declarative", // 函數(shù)環(huán)境// 標(biāo)識(shí)符綁定在這里 // 對(duì)外部環(huán)境的引用outer: <Global or outer function environment reference> } }復(fù)制代碼在創(chuàng)建變量對(duì)象,也就是創(chuàng)建變量環(huán)境,而變量環(huán)境也是一個(gè)詞法環(huán)境。在 ES6 中,詞法 環(huán)境和 變量 環(huán)境的區(qū)別在于前者用于存儲(chǔ)函數(shù)聲明和變量( let 和 const )綁定,而后者僅用于存儲(chǔ)變量( var )綁定。
如例子:
let a = 20; const b = 30; var c;function multiply(e, f) { var g = 20; return e * f * g; }c = multiply(20, 30);復(fù)制代碼執(zhí)行上下文如下所示
GlobalExectionContext = {ThisBinding: <Global Object>,LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 標(biāo)識(shí)符綁定在這里 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> },VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 標(biāo)識(shí)符綁定在這里 c: undefined, } outer: <null> } }FunctionExectionContext = { ThisBinding: <Global Object>,LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標(biāo)識(shí)符綁定在這里 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> },VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標(biāo)識(shí)符綁定在這里 g: undefined }, outer: <GlobalLexicalEnvironment> } }復(fù)制代碼變量提升的具體原因:在創(chuàng)建階段,函數(shù)聲明存儲(chǔ)在環(huán)境中,而變量會(huì)被設(shè)置為 undefined(在 var 的情況下)或保持未初始化(在 let 和 const 的情況下)。所以這就是為什么可以在聲明之前訪問 var 定義的變量(盡管是 undefined ),但如果在聲明之前訪問 let 和 const 定義的變量就會(huì)提示引用錯(cuò)誤的原因。此時(shí)let 和 const處于未初始化狀態(tài)不能使用,只有進(jìn)入執(zhí)行階段,變量對(duì)象中的變量屬性進(jìn)行賦值后,變量對(duì)象(Variable Object)轉(zhuǎn)為活動(dòng)對(duì)象(Active Object)后,let和const才能進(jìn)行訪問。
關(guān)于函數(shù)聲明和變量聲明,這篇文章講的很好:github.com/yygmind/blo…
另外關(guān)于閉包的理解,如例子:
function foo() {var num = 20;function bar() {var result = num + 20;return result}bar() }foo()復(fù)制代碼瀏覽器分析如下:
chrome瀏覽器理解閉包是foo,那么按瀏覽器的標(biāo)準(zhǔn)是如何定義閉包的,總結(jié)為三點(diǎn):
在函數(shù)內(nèi)部定義新函數(shù)
新函數(shù)訪問外層函數(shù)的局部變量,即訪問外層函數(shù)環(huán)境的活動(dòng)對(duì)象屬性
新函數(shù)執(zhí)行,創(chuàng)建新的函數(shù)執(zhí)行上下文,外層函數(shù)即為閉包
3.3 this指向
比較復(fù)雜,后面專門弄一篇文章來(lái)整理。
三. 執(zhí)行階段
1. 網(wǎng)頁(yè)的線程
永遠(yuǎn)只有JS引擎線程在執(zhí)行JS腳本程序,其他三個(gè)線程只負(fù)責(zé)將滿足觸發(fā)條件的處理函數(shù)推進(jìn)事件隊(duì)列,等待JS引擎線程執(zhí)行, 不參與代碼解析與執(zhí)行。
JS引擎線程: 也稱為JS內(nèi)核,負(fù)責(zé)解析執(zhí)行Javascript腳本程序的主線程(例如V8引擎)
事件觸發(fā)線程: 歸屬于瀏覽器內(nèi)核進(jìn)程,不受JS引擎線程控制。主要用于控制事件(例如鼠標(biāo),鍵盤等事件),當(dāng)該事件被觸發(fā)時(shí)候,事件觸發(fā)線程就會(huì)把該事件的處理函數(shù)推進(jìn)事件隊(duì)列,等待JS引擎線程執(zhí)行
定時(shí)器觸發(fā)線程:主要控制計(jì)時(shí)器setInterval和延時(shí)器setTimeout,用于定時(shí)器的計(jì)時(shí),計(jì)時(shí)完畢,滿足定時(shí)器的觸發(fā)條件,則將定時(shí)器的處理函數(shù)推進(jìn)事件隊(duì)列中,等待JS引擎線程執(zhí)行。 注:W3C在HTML標(biāo)準(zhǔn)中規(guī)定setTimeout低于4ms的時(shí)間間隔算為4ms。
HTTP異步請(qǐng)求線程:通過XMLHttpRequest連接后,通過瀏覽器新開的一個(gè)線程,監(jiān)控readyState狀態(tài)變更時(shí),如果設(shè)置了該狀態(tài)的回調(diào)函數(shù),則將該狀態(tài)的處理函數(shù)推進(jìn)事件隊(duì)列中,等待JS引擎線程執(zhí)行。 注:瀏覽器對(duì)通一域名請(qǐng)求的并發(fā)連接數(shù)是有限制的,Chrome和Firefox限制數(shù)為6個(gè),ie8則為10個(gè)。
2. 宏任務(wù)
宏任務(wù)(macro-task)可分為同步任務(wù)和異步任務(wù):
同步任務(wù)指的是在JS引擎主線程上按順序執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢后,才能執(zhí)行后一個(gè)任務(wù),形成一個(gè)執(zhí)行棧(函數(shù)調(diào)用棧)。
異步任務(wù)指的是不直接進(jìn)入JS引擎主線程,而是滿足觸發(fā)條件時(shí),相關(guān)的線程將該異步任務(wù)推進(jìn)任務(wù)隊(duì)列(task queue),等待JS引擎主線程上的任務(wù)執(zhí)行完畢,空閑時(shí)讀取執(zhí)行的任務(wù),例如異步Ajax,DOM事件,setTimeout等。
理解宏任務(wù)中同步任務(wù)和異步任務(wù)的執(zhí)行順序,那么就相當(dāng)于理解了JS異步執(zhí)行機(jī)制–事件循環(huán)(Event Loop)。
3. 事件循環(huán)
事件循環(huán)可以理解成由三部分組成,分別是:
主線程執(zhí)行棧
異步任務(wù)等待觸發(fā)
任務(wù)隊(duì)列
任務(wù)隊(duì)列(task queue)就是以隊(duì)列的數(shù)據(jù)結(jié)構(gòu)對(duì)事件任務(wù)進(jìn)行管理,特點(diǎn)是先進(jìn)先出,后進(jìn)后出。
setTimeout和setInterval的區(qū)別:
setTimeout是在到了指定時(shí)間的時(shí)候就把事件推到任務(wù)隊(duì)列中,只有當(dāng)在任務(wù)隊(duì)列中的setTimeout事件被主線程執(zhí)行后,才會(huì)繼續(xù)再次在到了指定時(shí)間的時(shí)候把事件推到任務(wù)隊(duì)列,那么setTimeout的事件執(zhí)行肯定比指定的時(shí)間要久,具體相差多少跟代碼執(zhí)行時(shí)間有關(guān)
setInterval則是每次都精確的隔一段時(shí)間就向任務(wù)隊(duì)列推入一個(gè)事件,無(wú)論上一個(gè)setInterval事件是否已經(jīng)執(zhí)行,所以有可能存在setInterval的事件任務(wù)累積,導(dǎo)致setInterval的代碼重復(fù)連續(xù)執(zhí)行多次,影響頁(yè)面性能。
4. 微任務(wù)
微任務(wù)是在es6和node環(huán)境中出現(xiàn)的一個(gè)任務(wù)類型,如果不考慮es6和node環(huán)境的話,我們只需要理解宏任務(wù)事件循環(huán)的執(zhí)行過程就已經(jīng)足夠了,但是到了es6和node環(huán)境,我們就需要理解微任務(wù)的執(zhí)行順序了。 微任務(wù)(micro-task)的API主要有:Promise, process.nextTick
例子理解:
console.log('script start');setTimeout(function() {console.log('setTimeout'); }, 0);Promise.resolve().then(function() {console.log('promise1'); }).then(function() {console.log('promise2'); });console.log('script end');復(fù)制代碼執(zhí)行過程如下:
代碼塊通過語(yǔ)法分析和預(yù)編譯后,進(jìn)入執(zhí)行階段,當(dāng)JS引擎主線程執(zhí)行到console.log('script start');,JS引擎主線程認(rèn)為該任務(wù)是同步任務(wù),所以立刻執(zhí)行輸出script start,然后繼續(xù)向下執(zhí)行;
JS引擎主線程執(zhí)行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主線程認(rèn)為setTimeout是異步任務(wù)API,則向?yàn)g覽器內(nèi)核進(jìn)程申請(qǐng)開啟定時(shí)器線程進(jìn)行計(jì)時(shí)和控制該setTimeout任務(wù)。由于W3C在HTML標(biāo)準(zhǔn)中規(guī)定setTimeout低于4ms的時(shí)間間隔算為4ms,那么當(dāng)計(jì)時(shí)到4ms時(shí),定時(shí)器線程就把該回調(diào)處理函數(shù)推進(jìn)任務(wù)隊(duì)列中等待主線程執(zhí)行,然后JS引擎主線程繼續(xù)向下執(zhí)行
JS引擎主線程執(zhí)行到Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });,JS引擎主線程認(rèn)為Promise是一個(gè)微任務(wù),這把該任務(wù)劃分為微任務(wù),等待執(zhí)行
JS引擎主線程執(zhí)行到console.log('script end');,JS引擎主線程認(rèn)為該任務(wù)是同步任務(wù),所以立刻執(zhí)行輸出script end
主線程上的宏任務(wù)執(zhí)行完畢,則開始檢測(cè)是否存在可執(zhí)行的微任務(wù),檢測(cè)到一個(gè)Promise微任務(wù),那么立刻執(zhí)行,輸出promise1和promise2
微任務(wù)執(zhí)行完畢,主線程開始讀取任務(wù)隊(duì)列中的事件任務(wù)setTimeout,推入主線程形成新宏任務(wù),然后在主線程中執(zhí)行,輸出setTimeout
最后的輸出結(jié)果即為:
script start script end promise1 promise2 setTimeout復(fù)制代碼文章參考:
github.com/yygmind/blo…
heyingye.github.io/2018/03/19/…
heyingye.github.io/2018/03/26/…
github.com/yygmind/blo…
轉(zhuǎn)載于:https://juejin.im/post/5c7a9b92518825153f784e14
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的JS引擎线程的执行过程的三个阶段的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: js 幻灯片
- 下一篇: JavaScript中十个一步拷贝数组的