JS運行機制之 Event Loop 的思考

先舉個栗子,以下:javascript

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log('i: ',i);        //一秒以後輸出幾乎沒有時間間隔依次輸出5個5
    }, 1000);
}
console.log(i);                      //當即輸出5

想必不少人看到立馬能看出答案吧,可是爲何定時器不能依次打印出0, 1,2,3,4呢?答案稍後分曉。java

那到底怎麼才能依次輸出咱們想要的結果呢?你們可能都想到是利用閉包,或者是利ES6中的let聲明,再或者能夠用Promise, 若是還不過癮就用ES7 的async或者await; 以下,可是今天咱們主要不講這個。web

//利用閉包
for (var i = 0; i < 5; i++) {
    (function(i) {
        setTimeout(function() {  
            console.log(i);   //0,1,2,3,4
        }, 1000);
    })(i);
}
 
console.log( i);             //5

//let
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log (i);
    }, 1000);        //0 ,1,2,3,4
}
 
console.log( i);     //這裏會報錯,由於let聲明的塊級做用域,外面是拿不到這個i的,使用沒有聲明的變量固然會報錯啦

  

1、爲何js是單線程?瀏覽器

你們都知道js不一樣於其餘語言,它是單線程的。那麼問題來了,爲何不是多線程呢?按道理來講多線程不是可以同時解決問題提升效率麼?除了多線程產生衝突、搶佔資源等答案,還能夠是什麼呢?多線程

其實,這跟它做爲瀏覽器腳本語言的用途有關,瀏覽器的腳本語言主要的用途是用來與用戶互動,會產生DOM的操做,這就是問題的關鍵,假設js是多線程,有一個線程是刪除DOM操做,有個在當前DOM添加內容,這時候瀏覽器應該怎麼辦呢?這就決定了js應該被設計成單線程。那js有沒有多線程的可能呢?答案是確定的,HTML5提出的web Worker標準,可是,閉包

「爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質」  ----阮一峯的一篇博客異步

 

二 、 Event Loop 事件循環async

由於js是單線程,因此執行時候須要排隊,前一個任務結束以後,後一個任務纔會執行,那麼若是前一個任務執行好久,後面一個任務就要等好久。若是一些等待不是由於CPU計算慢產生的,好比IO設備的使用,那麼js會把等待的任務掛起,執行後面的任務,等IO返回結果,再回頭執行直線掛起的任務。函數

這樣任務就有了同步任務和異步任務,同步任務是主線程上執行的任務,造成一個執行棧(execution context stack),是排隊進行的。異步任務不是在主線程執行的,是在「任務隊列」(Queue)裏面,只有當主線程全部同步的任務執行完成,任務隊列中的異步任務纔會進入主線程,而後被執行。oop

異步執行機制以下:---阮一峯

 

可視化描述---MDN

主線程上:

一個函數1被調用了,建立一個堆棧幀,包含了函數1的參數和局部變量,當函數1中又調用了函數2,又建立了一個堆棧幀,此時這個堆棧幀在第一個堆棧幀以前,包含了函數2的參數和局部變量。主線程先執行了至於頂層的堆棧幀(函數2產生的),當函數2返回時,對應的堆棧幀就出棧了,接着繼續執行函數1的堆棧幀,直到棧空了。

消息隊列:
一個 js運行時包含了一個待處理的消息隊列。每個消息都與一個函數相關聯。當棧爲空時,從隊列中取出一個消息進行處理。這個處理過程包含了調用與這個消息相關聯的函數(以及於是建立了一個初始堆棧幀)。當棧再次爲空的時候,也就意味着消息處理結束。 

添加消息:

在瀏覽器裏,添加事件能夠是當一個事件出現且有一個事件監聽器被綁定時,消息被隨時添加。也能夠是在調用setTimeout等函數時候,

在未來的某個時間後在消息對列中添加。

setTimeout:

調用該函數時候會在未來的某個時間後在消息對列中添加一個消息,若是執行棧中有其餘任務沒有完成(假設有一個很耗時的計算),setTimeout消息必須必須等到執行棧的任務完成纔會處理,因此說該函數的第二個參數僅僅表示最少的時間 而非確切的時間。

即便你設置零延遲:

 setTimeout(function cb1() {
    console.log(‘我是第二個被執行的’);
  }, 0);                           
 console.log(‘我是第一個被執行的’);       //先打印這句    
          

事實上,js中規定,定時器的第二個參數設置最少不能小於4ms, 小於的話就按最小的4ms執行。

 

上圖中,主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各類外部API,它們在"任

務隊列"中加入各類事件(click,load)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。

 

3、回頭看看開頭的栗子

for循環和外面的console.log()是主線程上的同步任務,他們按循序執行,先執行for循環結束(此時i變爲5),再執行console.log();

for循環中的setTimeout是在任務隊列中,它只有等在前主線程中的棧空了以後,到一個時間纔會被被執行,此時做用域中的i已經變爲5,此時setTimeout中的回調函數所讀取的i就是5了。

還有一個問題?

輸出5的時間間隔是多少?答案顯而易見是,當即打印一個5,1000ms以後幾乎同時輸出5個5; why???

正如上面解釋的,for循環一次,會在任務隊列中加上一個setTimeou任務(該任務是在1000ms後執行回調函數),這樣循環結束,任務列表裏面就有了5個setTimeou任務,且當主線程中棧空了以後,任務列表就開始進棧,等待1000ms以後執行回調,(注意此時的i變量已經變成5了)因此後面的5個5幾乎在同時依次打印出來。

 

4、任務隊列

JS分爲同步任務和異步任務;

同步任務都是在主線程上執行,造成一個執行棧;

主線程以後,事件觸發線程管理着一個任務隊列【宏任務(MacroTask)和微任務(MicroTask)】,只要異步任務有了運行結果,就會往任務隊列裏面放置一個事件。

一旦執行棧中的全部同步任務執行完畢,此時JS引擎空閒,系統就會讀取任務隊列,將以前異步任務的結果(事件)添加到可執行棧中,開始執行。

4.1 宏任務

task(又稱之爲宏任務),能夠理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)。

瀏覽器爲了可以使得JS內部task與DOM任務可以有序的執行,會在一個task執行結束後,在下一個(macro)task 執行開始前,對頁面進行從新渲染,流程以下:

task主要包含:script(總體代碼)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 環境)

4.2微任務

microtask(又稱爲微任務),能夠理解是在當前 task 執行結束後當即執行的任務。也就是說,在當前task任務後,下一個task以前,在渲染以前。

因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染。也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前)。

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 環境)

4.3運行機制

在事件循環中,每進行一次循環操做稱爲 tick,但關鍵步驟以下:

  • 執行全部執行棧中的任務(宏任務)
  • 執行棧中執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  • 當執行棧空了以後,當即執行當前微任務隊列中的全部微任務(依次執行)
  • 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
相關文章
相關標籤/搜索