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