精讀Javascript系列(三) 執行上下文、 執行棧、初識事件循環

前言

這時可以接觸真正實用的東西了,畢竟變量也不能代表整個Javascript語言,雖然有些不可思議,但變量的確是Javascript必經之路之一,關於變量的奇特行爲數不勝數(真的是這樣),不過這些我想高階Javascript都努力迴避這些,新手也不懂,所以我就跳過了。

下面的這些概念,無論是執行上下文、 還是執行棧,它在規範中的概念都很抽象,很多內容的理解實際靠的都是想象力,若有錯誤之處,還請指正。

執行上下文

簡而言之,執行上下文(Execution Context)是正在運行的可執行代碼所處環境的抽象,用於追蹤一段代碼中變量的求值。這是我總結過來的概念,可能有些不準確,也可以參考真正的規範定義。

不過總的來說,有三個關鍵點:

  1. 只有可執行代碼纔會有執行上下文
  2. 執行上下文是有狀態的:運行狀態(Perform)、掛起狀態(Suspend)以及恢復(Resume)。處於Perfrom狀態的執行上下文稱之爲運行時執行上下文(Running Execution Context)
  3. 執行上下文完全不等價於詞法環境,硬說關係,也只是前者引用了後者而已。
  4. 執行一個JS腳本時,可以有多個執行上下文存在,但是 運行時上下文只有唯一一個(異步也是如此,至於爲什麼提了四個……三大天王有四個不是常識麼……)。

並且ES規範中規定,可執行代碼有下面幾個:

  1. 全局代碼
  2. 函數代碼
  3. Eval語句
  4. 模塊代碼

換言之,看以下代碼:

  
  var g=111
  function f(){
      
      console.log(g);
      for(let i =0; i < 10; i++){
          console.log(i);
      }
  }
  f();  /// (*)    

如果上面的代碼運行,只會產生兩個執行上下文:

  1. 全局
  2. 函數f

但是如果將標註(*)的行註釋掉,那麼最終只有一個執行上下文,因爲函數f根本就不會執行,自然就不會有相應的執行上下文了。裏面唯一一個迷惑的是,就是for-loop了,但它根本不是可執行代碼,所以它是函數執行上下文的一部分

執行上下文的重要組成部分

一個執行上下文,可以抽象爲:

ExecutionContext = {
    State: <>
    LexEnv = {
        This:< ... > ,
        OuterEnv:< ... > ,
        DecRec:{
            //... identifiername-variable
        }      
    }
    VaEnv = {
        This:< ... > ,
        OuterEnv:< ... > ,
        VarRec:{
            //... identifiername-variable
        }      
    }
}

事實上,在一個執行上下文中有兩個相當重要的組件:LexicalEnvironmentComponent(詞法環境組件)和VariableEnvironmentComponent(變量環境組件)。詞法環境組件指向當前代碼中的詞法環境LexEnv), 變量環境組件指向當前代碼變量環境VarEnv)。

關於執行上下文不得不說的二三事中,有一個很重要的部分就是作用域鏈,但是在執行上下文中並沒有看到相關內容。不過作用域鏈的確存在,它就在[[Scope]]內部屬性中,通過瀏覽器可以直接看到。

不過也可以這樣理解,在一個執行上下文被創建時,不僅會創建當前詞法環境的LexEnv,也會創建LexEnv.OutEnvLexEnv.OutEnv.OutEnv…,直至延伸到全局爲止。

執行上下文的創建與銷燬
  1. 創建一個新執行上下文(ExecutionContext , EC)
  2. 創建當前詞法環境(LexEnv 和 VarEnv )
  3. 將該執行上下文的 LexicalEnvironmentComponent
    VariableEnvironmentComponent 指向當前環境下的 LexEnvVarEnv中。
  4. 將新執行上下文推入 執行棧中,併成爲運行時執行上下文
  5. 對可執行代碼塊內的標識符進行實例化和初始化:
    1. 收集當前詞法環境內所有聲明的標識符歸入DecRec中,所有var聲明的標識符歸入VarNames集合中,在此階段會進行標識符名檢測,若與let/const/...聲明的標識符與VarNames中的標識符重複, 報錯。
    2. DecRec中的標識符進行實例化,並設爲uninitializedVarNames中的標識符綁定在ObjRec中,實例化後直接初始化爲undefined
    3. 對於function聲明的函數,將直接指向函數對象,並也會綁定到ObjRec中,這是瀏覽器默認行爲
  6. 運行代碼。
    1. 非var聲明的標識符會在聲明處進行初始化(默認爲undefined)。
    2. 完成所有變量的賦值,並可能會一直在變化。
  7. 運行完畢從 執行棧 中彈出。
備註:
  • 關於This綁定,大部分情況可以用過去的說法解釋,然而某些情況下卻不盡然。
  • 閉包我會在下一篇介紹。
  • 執行上下文,我個人認爲並不如何重要,但是卻能在許多情形下起到極爲關鍵的作用,所以還是有必要去深入認識一下。
  • 關於執行上下文和詞法環境的關係,最多是前者引用了後者,僅此而已。誠然,有許多情況沒必要用執行上下文來說明,但是永遠避免不了違和感

執行棧與事件循環

執行棧(Execution Stack)就是由執行上下文構成的堆棧,類似於Call Stack

  1. Javascript引擎遇到一段可執行代碼時,新建一個執行上下文。
  2. 將它推入執行棧中。並設置爲運行時執行上下文。
    • 如果存在其他執行上下文。
    • 那麼將當前執行上下文掛起
    • 然後再將新執行上下文推入執行棧中。
  3. 執行上下文運行完畢,彈出銷燬恢復並將原執行上下文設爲運行時。

總覺得這些沒什麼好說的,但是水一下吧

執行棧最重要的部分並非是執行棧概念本身,而是與任務隊列的關係,它是事件循環的入門關鍵概念之一

衆所周知,Javascript語言是單線程的,此處的執行棧就相當於主線程的調用棧,也是唯一一個調用棧,至於什麼是主線程可以查閱相關資料,這裏有些超綱了……

那麼javascript是如何實現異步的

確切來說,這不是Javascript核心的部分,它是結合瀏覽器API(如Web Worker, Browser-context瞭解一下)實現的

事件循環中(事件處理過程),有兩個極其重要的概念:

  • 任務序列: Task Quenue
  • 事件: Event

這兩個概念,是抽象滴。

在Javascript中,一個任務也可以稱之爲事件,通常是一個函數回調,由許多任務組成的隊列,就是所謂的任務序列了。任務序列有很多分類,例如:作業序列(Job Quenue)、消息序列(Message Quenue),本質沒區別。

不必再深入瞭解,現在需要記住的是:一個任務序列中的任務如果想要被執行,就必須將它取出放入執行棧中。

舉一個抽象點的例子:

例如下面的代碼:

  
      var temp = 10;
      
      console.log('push task1');
      setTimeout(function task1(){
        temp+=10;
        console.log(temp+'task1 okay! ');
      },1000)
      
      console.log('taskquenue=[task1]; push task2');
      setTimeout(function task2(){
        temp*=10;
        console.log(temp+'task2 okay! ');
      },500) 
     
      console.log('taskquenue=[task1,task2]; push task3');
      setTimeout(function task3(){
        temp*= -0.2;
        console.log(temp+'task3 okay! ');
      },1500)
      console.log('taskquenue=[task1, task2,task3]');

輸出如下:

push task1
taskquenue=[task1]; push task2
taskquenue=[task1,task2]; push task3
taskquenue=[task1, task2,task3]
100task2 okay! 
110task1 okay! 
-22task3 okay! 

setTimeout是一個定時器,它能夠將任務放到任務隊列中。如圖:

  • 添加作業task1
    在這裏插入圖片描述
  • 添加作業task2
    在這裏插入圖片描述
  • 添加作業task3
    在這裏插入圖片描述

執行到此處, task1task2task3都被放入了任務隊列; 然後執行棧全部執行完畢後,開始處理任務隊列中的任務。

爲什麼任務隊列中的任務必須在執行棧空時後執行呢?

  • 這裏我也不清楚,這畢竟真正涉及了底層內容;只是有些明白爲何這樣,不過擔心誤導他人,就算了。
  • 一般而言, 關於任務序列的相關概念沒有如此簡單,還涉及了很多東西,例如阻塞調度(Schedule)等,這些方面可以參考其他諸如C++Java這類多線程的語言或是看看操作系統這方面的內容。
  • 如果只是簡單的業務實現,不需要了解這麼多底層的東西。

現在開始處理任務吧:

  • 處理task2
    在這裏插入圖片描述
  • 處理task1
    在這裏插入圖片描述
  • 處理task3
    在這裏插入圖片描述

好了,一個事件循環就這麼結束了。
然後Javascript引擎進入休眠階段Javascript引擎永不結束!),等待有新的任務執行,然後開始下一個事件循環。

備註:
  • 這裏只是一個簡單例子
  • 事件循環可以有多個任務隊列
  • 任務序列共分爲兩種: 微任務序列和宏任務序列
  • 我們的script代碼,就是宏任務序列之一。

最後: Javascript引擎

這是我精讀Javascript系列第三篇,猝不及防的就到了事件循環,看起來一下子就深入好多好多…… 但是我覺得這纔是最合理程安排,大多的文檔都把任務序列調用棧給分開了,但是在~~_____~~,它們本應該就是一體,不應該以任何方便的理由將它們分開

深入Javascript, 除了看規範,最好也看下JS引擎的實現文檔,有些進階內容,不在規範中,而是在這些文檔裏(請自己Google查找,百度也能搜索到)。

如果對Javascript引擎比較感興趣的,可以參考:

雖然,極其不建議新手一下子看V8源代碼,那種頭痛欲裂又渾身顫抖不止的感覺實在是過癮之極啊……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章