瀏覽器首先按順序加載由<script>標籤分割的js代碼塊,加載js代碼塊完畢後,馬上進入如下三個階段,而後再按順序查找下一個代碼塊,再繼續執行如下三個階段,不管是外部腳本文件(不異步加載)仍是內部腳本代碼塊,都是同樣的原理,而且都在同一個全局做用域中。node
JS引擎線程的執行過程的三個階段:git
分析該js腳本代碼塊的語法是否正確,若是出現不正確,則向外拋出一個語法錯誤(SyntaxError),中止該js代碼塊的執行,而後繼續查找並加載下一個代碼塊;若是語法正確,則進入預編譯階段。es6
下面階段的代碼執行不會再進行語法校驗,語法分析在代碼塊加載完畢時統一檢驗語法。github
全局環境(JS代碼加載完畢後,進入代碼預編譯即進入全局環境)chrome
函數環境(函數調用執行時,進入該函數環境,不一樣的函數則函數環境不一樣)promise
eval(不建議使用,會有安全,性能等問題)瀏覽器
每進入一個不一樣的運行環境都會建立一個相應的執行上下文(Execution Context),那麼在一段JS程序中通常都會建立多個執行上下文,js引擎會以棧的方式對這些執行上下文進行處理,造成函數調用棧(call stack),棧底永遠是全局執行上下文(Global Execution Context),棧頂則永遠是當前執行上下文。安全
調用棧,也叫執行棧,具備LIFO(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。bash
首次運行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複製代碼
執行上下文可理解爲當前的執行環境,與該運行環境相對應,具體分類如上面所說分爲全局執行上下文和函數執行上下文。建立執行上下文的三部曲:
建立變量對象(Variable Object)
創建做用域鏈(Scope Chain)
肯定this的指向
建立arguments對象:檢查當前上下文中的參數,創建該對象的屬性與屬性值,僅在函數環境(非箭頭函數)中進行,全局環境沒有此過程
檢查當前上下文的函數聲明:按代碼順序查找,將找到的函數提早聲明,若是當前上下文的變量對象沒有該函數名屬性,則在該變量對象以函數名創建一個屬性,屬性值則爲指向該函數所在堆內存地址的引用,若是存在,則會被新的引用覆蓋。
檢查當前上下文的變量聲明:按代碼順序查找,將找到的變量提早聲明,若是當前上下文的變量對象沒有該變量名屬性,則在該變量對象以變量名創建一個屬性,屬性值爲undefined;若是存在,則忽略該變量聲明
函數聲明提早和變量聲明提高是在建立變量對象中進行的,且函數聲明優先級高於變量聲明。具體是如何函數和變量聲明提早的能夠看後面。
建立變量對象發生在預編譯階段,但還沒有進入執行階段,該變量對象都是不能訪問的,由於此時的變量對象中的變量屬性還沒有賦值,值仍爲undefined,只有進入執行階段,變量對象中的變量屬性進行賦值後,變量對象(Variable Object)轉爲活動對象(Active Object)後,才能進行訪問,這個過程就是VO –> AO過程。
通俗理解,做用域鏈由當前執行環境的變量對象(未進入執行階段前)與上層環境的一系列活動對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。
能夠經過一個例子簡單理解:
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,那麼按瀏覽器的標準是如何定義閉包的,總結爲三點:
在函數內部定義新函數
新函數訪問外層函數的局部變量,即訪問外層函數環境的活動對象屬性
新函數執行,建立新的函數執行上下文,外層函數即爲閉包
比較複雜,後面專門弄一篇文章來整理。
永遠只有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個。
宏任務(macro-task)可分爲同步任務和異步任務:
同步任務指的是在JS引擎主線程上按順序執行的任務,只有前一個任務執行完畢後,才能執行後一個任務,造成一個執行棧(函數調用棧)。
異步任務指的是不直接進入JS引擎主線程,而是知足觸發條件時,相關的線程將該異步任務推動任務隊列(task queue),等待JS引擎主線程上的任務執行完畢,空閒時讀取執行的任務,例如異步Ajax,DOM事件,setTimeout等。
理解宏任務中同步任務和異步任務的執行順序,那麼就至關於理解了JS異步執行機制–事件循環(Event Loop)。
事件循環能夠理解成由三部分組成,分別是:
主線程執行棧
異步任務等待觸發
任務隊列
任務隊列(task queue)就是以隊列的數據結構對事件任務進行管理,特色是先進先出,後進後出。
setTimeout和setInterval的區別:
setTimeout是在到了指定時間的時候就把事件推到任務隊列中,只有當在任務隊列中的setTimeout事件被主線程執行後,纔會繼續再次在到了指定時間的時候把事件推到任務隊列,那麼setTimeout的事件執行確定比指定的時間要久,具體相差多少跟代碼執行時間有關
setInterval則是每次都精確的隔一段時間就向任務隊列推入一個事件,不管上一個setInterval事件是否已經執行,因此有可能存在setInterval的事件任務累積,致使setInterval的代碼重複連續執行屢次,影響頁面性能。
微任務是在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複製代碼
文章參考:
heyingye.github.io/2018/03/19/…