1. 程式人生 > >js執行機制及非同步程式設計(一)

js執行機制及非同步程式設計(一)

相信大家在面試的過程中經常遇到檢視執行順序的問題,如setTimeout,promise,async await等等,各種組合,是不是感覺頭都要暈掉了,其實這些問題最終還是考察大家對js的執行機制是否掌握牢固,對promise,async的原理是否掌握,萬變不離其宗,這次就來徹底搞懂它。

1 js引擎的執行原理

js引擎也是程式,是屬於瀏覽器的一部分,由瀏覽器廠商自行開發。從頭到尾負責整個JavaScript程式的編譯及執行過程

瀏覽器在渲染的過程中,首先按順序載入由<script>標籤分割的js程式碼塊,載入js程式碼塊完畢後,需要js引擎進行解析。無論是外部指令碼檔案(不非同步載入)還是內部指令碼程式碼塊,都是一樣的原理,並且都在同一個全域性作用域中。

JavaScript被歸類為“動態”或“解釋執行”語言,所以它無需提前編譯,而是由直譯器實時執行

js引擎執行過程分為三個階段:

  • JS的解釋階段
  • JS的預處理(編譯)階段及執行階段

1.1 JS的解釋階段

js指令碼程式碼塊載入完畢後,會首先JS的解釋階段。該階段主要過程如下:

  1. 詞法分析——這個過程會將由字元組成的字串分解成(對程式語言來說)有意義的程式碼塊,這些程式碼塊被稱為詞法單元(token)
  2. 語法分析——這個過程是將詞法單元流(陣列)轉化成抽象語法樹(Abstract Syntax Tree)
  3. 使用翻譯器(translator),將程式碼轉為位元組碼(bytecode)
  4. 使用位元組碼直譯器(bytecode interpreter),將位元組碼轉為機器碼

最終計算機執行的就是機器碼。

為了提高執行速度,現代瀏覽器一般採用即時編譯(JIT-Just In Time compiler)

即位元組碼只在執行時編譯,用到哪一行就編譯哪一行,並且把編譯結果快取(inline cache)

這樣整個程式的執行速度能得到顯著提升。

而且,不同瀏覽器策略可能還不同,有的瀏覽器就省略了位元組碼的翻譯步驟,直接轉為機器碼(如chrome的v8)

1.2 JS的預處理(編譯)階段及執行階段

這裡我理解為js為解釋型語言,由直譯器實時執行,通俗的說就是預處理完之後馬上執行,一邊編譯一邊執行

1.2.1 js的執行環境主要有三種:

  1. 全域性環境
  2. 函式環境
  3. eval(不建議使用,會有安全,效能問題)

1.2.2 以下段例子說明js的預編譯與執行過程


function bar() {
    var B_context = "Bar EC";

    function foo() {
        var f_context = "foo EC";
    }

    foo()
}

bar()

這段函式經過詞法解析,語法解析階段之後,就開始進入預編譯並執行,如下:

  1. 首先,進入全域性環境,就會先進行預處理,然建立全域性上下文執行環境(Global ExecutionContext),會對var宣告的變數和函式宣告進行預處理,window物件就是全域性執行上下文的變數物件,所有的變數和函式都是window物件的屬性方法。所以函式宣告提前和變數宣告提升是在建立變數物件中進行的,且函式宣告優先順序高於變數宣告。然後推入stack棧中。預處完成之後,開始執行js
  2. 當執行bar()時,就會進入bar函式執行環境,就會先進行預處理,建立bar函式執行上下文(bar Execution Context),推入stack棧中,預處理完後,開始執行foo()
  3. 在bar函式內部呼叫foo函式,則再進入foo函式執行環境,建立foo函式執行上下文(foo Execution Context),推入stack棧中
  4. 此刻棧底是全域性執行上下文(Global Execution Context),棧頂是foo函式執行上下文(foo Execution Context),如上圖,由於foo函式內部沒有再呼叫其他函式,那麼則開始出棧
  5. foo函式執行完畢後,棧頂foo函式執行上下文(foo Execution Context)首先出棧
  6. bar函式執行完畢,bar函式執行上下文(bar Execution Context)出棧
  7. Global Execution Context則在瀏覽器或者該標籤頁關閉時出棧。

1.2.3 執行上下文

分析一段簡單的程式碼,幫助我們理解建立執行上下文的過程,如下:


function fun(a, b) {
    var num = 1;

    function test() {

        console.log(num)

    }
}

fun(2, 3)

這裡我們在全域性環境呼叫fun函式,建立fun執行上下文,這裡為了方便大家理解,暫時不講解作用域鏈以及this指向,如下:


funEC = {
    //變數物件
    VO: {
        //arguments物件
        arguments: {
            a: undefined,
            b: undefined,
            length: 2
        },

        //test函式
        test: &lt;test reference&gt;, 

        //num變數
        num: undefined
    },

    //作用域鏈
    scopeChain:[],

    //this指向
    this: window
}
  • funEC表示fun函式的執行上下文(fun Execution Context簡寫為funEC)
  • funE的變數物件中arguments屬性,上面的寫法僅為了方便大家理解,但是在瀏覽器中展示是以類陣列的方式展示的
  • <test reference>表示test函式在堆記憶體地址的引用
注:建立變數物件發生在預編譯階段,但尚未進入執行階段,該變數物件都是不能訪問的,因為此時的變數物件中的變數屬性尚未賦值,值仍為undefined,只有進入執行階段,變數物件中的變數屬性進行賦值後,變數物件(Variable
Object)轉為活動物件(Active Object)後,才能進行訪問,這個過程就是VO –> AO過程。

建立作用域鏈
作用域鏈由當前執行環境的變數物件(未進入執行階段前)與上層環境的一系列活動物件組成,它保證了當前執行環境對符合訪問許可權的變數和函式的有序訪問。

理清作用域鏈可以幫助我們理解js很多問題包括閉包問題等,下面我們結合一個簡單的例子來理解作用域鏈,如下:


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
}

在這裡我們順便思考一下,什麼是閉包?
我們先看下面一個簡單例子,如下:


function foo() {
    var num = 20;

    function bar() {
        var result = num + 20;

        return result
    }

    bar()
}

foo()

我這裡直接以瀏覽器解析,以瀏覽器理解的閉包為準來分析閉包,如下圖:

如上圖所示,chrome瀏覽器理解閉包是foo,那麼按瀏覽器的標準是如何定義閉包的,我總結為三點:

  • 在函式內部定義新函式
  • 新函式訪問外層函式的區域性變數,即訪問外層函式環境的活動物件屬性
  • 新函式執行,建立新的函式執行上下文,外層函式即為閉包

確定this指向
在全域性環境下,全域性執行上下文中變數物件的this屬性指向為window;函式環境下的this指向卻較為靈活,需根據執行環境和執行方法確定

來源:https://segmentfault.com/a/1190000017394625