深刻理解 JavaScript 事件循環(一)— event loop

 引言

  相信全部學過 JavaScript 都知道它是一門單線程的語言,這也就意味着 JS 沒法進行多線程編程,可是 JS 當中卻有着無處不在的異步概念 。在初期許多人會把異步理解成相似多線程的編程模式,其實他們中有着很大的差異,要徹底理解異步,就須要瞭解 JS 的運行核心——事件循環(event loop)。在以前我對事件循環的認識也是隻知其一;不知其二的,直到我看了 Philip Roberts 的演講 What the heck is the event loop anyway?,我纔對事件循環有了一個全面的認識,因此我想寫一篇介紹 JS 事件循環的文章,以供你們學習和參考。ajax

 1、爲何會有異步?

  爲何 JS 當中會有異步?咱們想象一下,若是咱們同步的執行一下代碼會發生什麼:編程

1 $.get(url, function(data) {
2     //do something
3 });

  在咱們使用 ajax 進行通訊的時候,咱們都默認了它是異步的,可是若是咱們設置其爲同步執行,會發生什麼?若是你本身寫一個小的測試程序,將後臺代碼延遲5s你會發現瀏覽器會出現阻塞,直到 ajax 響應了以後纔會正常運行。這即是異步模式要解決的首要問題,如何使瀏覽器非阻塞的運行任務。想象一下若是咱們同步的執行 ajax 請求的話,咱們的等待的時間是一個未知數,在網絡通訊中可能很快也可能很慢,也可能永遠也不會響應,這也就會致使瀏覽器會阻塞在一個未知的任務上面,這也是咱們不但願看到的。因此咱們但願有一種方式可以異步的處理程序,咱們並不須要關心一個 ajax 請求會在什麼時候完成,甚至它能夠永遠不會響應,咱們只須要知道在請求響應後該如何處理,而且在等待響應的這段時間內咱們還能夠作一些其餘的工做。所以,便有了 JavaScript Event Loop。數組

 2、什麼是事件隊列?

  首先,咱們先來看一段簡單的代碼:瀏覽器

1 console.log("script start");
2 
3 setTimeout(function () {
4     console.log("setTimeout");
5 }, 1000);
6 
7 console.log("script end");

  你能夠在這裏查看結果:網絡

   咱們能夠看到,首先,程序輸出 'script start''script end',在大約1s以後輸出了 'setTimeout' 。該程序的 'script end' 並無等待1s以後輸出,而是當即輸出。這是由於 setTimeout 是一個異步的函數。意思也就是說當咱們設置一個延遲函數的時候,當前腳本並不會阻塞,它只是會在瀏覽器的事件表中進行記錄,程序會繼續向下執行。當延遲的時間結束以後,事件表會將回調函數添加至事件隊列(task queue)中,事件隊列拿到了任務事後便將任務壓入執行棧(stack)當中,執行棧執行任務,輸出 'setTimeout'多線程

  事件隊列是一個存儲着待執行任務的隊列,其中的任務嚴格按照時間前後順序執行,排在隊頭的任務將會率先執行,而排在隊尾的任務會最後執行。事件隊列每次僅執行一個任務,在該任務執行完畢以後,再執行下一個任務。執行棧則是一個相似於函數調用棧的運行容器,當執行棧爲空時,JS 引擎便檢查事件隊列,若是不爲空的話,事件隊列便將第一個任務壓入執行棧中運行。異步

  如今,咱們對上面的代碼進行一點修改:async

1 console.log("script start");
2 
3 setTimeout(function () {
4     console.log("setTimeout");
5 }, 0);
6 
7 console.log("script end");

  將延遲時間設置爲0,看看程序會以何種順序輸出?不管咱們設置多少的延遲時間,'setTimeout' 老是會在 'script end' 以後輸出。有些瀏覽器可能會有一個最小延遲時間,有的是 15ms,有的是 10ms,這個在不少書當中都有提到,這可能會給同窗們形成一種錯覺:因爲程序運行速度很快,而且有最小延遲時間,因此 'setTimeout' 會在 'script end' 以後輸出。如今讓咱們在稍微變一下,來消除你的錯覺:函數

 1 console.log("script start");
 2 
 3 setTimeout(function () {
 4     console.log("setTimeout");
 5 }, 0);
 6 
 7 //具體數字不定,這取決於你的硬件配置和瀏覽器
 8 for(var i = 0; i < 999999999; i ++){
 9     //do something
10 }
11 
12 console.log("script end");

  你能夠在這裏查看結果:oop

   能夠看出,不管後面咱們作了多少延遲性的工做,'setTimeout' 老是會在 'script end' 以後輸出。因此究竟發生了什麼?這是由於 setTimeout 的回調函數只是會被添加至事件隊列,而不是當即執行。因爲當前的任務沒有執行結束,因此 setTimeout 任務不會執行,直到輸出了 'script end' 以後,當前任務執行完畢,執行棧爲空,這時事件隊列纔會把 setTimeout 回調函數壓入執行棧執行。

  執行棧則像是函數的調用棧,是一個樹狀的棧:

 3、事件隊列有何做用?

  經過以上的 demo 相信同窗們都會對事件隊列和執行棧有了一個基本的認識,那麼事件隊列有何做用?最簡單易懂的一點就是以前咱們所提到的異步問題。因爲 JS 是單線程的,同步執行任務會形成瀏覽器的阻塞,因此咱們將 JS 分紅一個又一個的任務,經過不停的循環來執行事件隊列中的任務。這就使得當咱們掛起某一個任務的時候能夠去作一些其餘的事情,而不須要等待這個任務執行完畢。因此事件循環的運行機制大體分爲如下步驟:

  1.   檢查事件隊列是否爲空,若是爲空,則繼續檢查;如不爲空,則執行 2;
  2.   取出事件隊列的首部,壓入執行棧;
  3.        執行任務;
  4.        檢查執行棧,若是執行棧爲空,則跳回第 1 步;如不爲空,則繼續檢查;

  然而目前爲止咱們討論的僅僅是 JS 引擎如何執行 JS 代碼,如今咱們結合 Web APIs 來討論事件循環在當中扮演的角色。

  在開始咱們討論過 ajax 技術的異步性和同步性,經過事件循環機制,咱們則不須要等待 ajax 響應以後再進行工做。咱們則是設置一個回調函數,將 ajax 請求掛起,而後繼續執行後面的代碼,至於請求什麼時候響應,對咱們的程序不會有影響,甚至它可能永遠也不響應,也不會使瀏覽器阻塞。而當響應成功了之後,瀏覽器的事件表則會將回調函數添加至事件隊列中等待執行。事件監聽器的回調函數也是一個任務,當咱們註冊了一個事件監聽器時,瀏覽器事件表會進行登記,當咱們觸發事件時,事件表便將回調函數添加至事件隊列當中。

  咱們知道 DOM 操做會觸發瀏覽器對文檔進行渲染,如修改排版規則,修改背景顏色等等,那麼這類操做是如何在瀏覽器當中奏效的?至此咱們已經知道了事件循環是如何執行的,事件循環器會不停的檢查事件隊列,若是不爲空,則取出隊首壓入執行棧執行。當一個任務執行完畢以後,事件循環器又會繼續不停的檢查事件隊列,不過在這間,瀏覽器會對頁面進行渲染。這就保證了用戶在瀏覽頁面的時候不會出現頁面阻塞的狀況,這也使 JS 動畫成爲可能, jQuery 動畫在底層均是使用 setTimeout 和 setInterval 來進行實現。想象一下若是咱們同步的執行動畫,那麼咱們不會看見任何漸變的效果,瀏覽器會在任務執行結束以後渲染窗口。反之咱們使用異步的方法,瀏覽器會在每個任務執行結束以後渲染窗口,這樣咱們就能看見動畫的漸變效果了。

  考慮以下兩種遍歷方式:

 1 var arr = new Array(999);
 2 arr.fill(1);
 3 function asyncForEach(array, handler){
 4     var t = setInterval(function () {
 5         if(array.length === 0){
 6             clearInterval(t);
 7         }else {
 8             handler(arr.shift());
 9         }
10     }, 0);
11 }
12 
13 //異步遍歷
14 asyncForEach(arr, function (value) {
15     console.log(value);
16 });
17 
18 //同步遍歷
19 arr.forEach(function (value, index, arr) {
20     console.log(value);
21 });

  通過測試,咱們能夠看出,採用同步遍歷的方法,當數組長度上升到3位數的時候,便會出現阻塞,可是異步遍歷卻不會出現阻塞現象(除非數組長度很是大,那是由於計算機的內存空間不足)。這是由於同步遍歷方法是一個單獨的任務,這個任務會將全部的數組元素遍歷一遍,而後纔會開始下一個任務。而異步遍歷的方法將每一次遍歷拆分紅一個單獨的任務,一個任務只遍歷一個數組元素,因此在每一個任務之間,咱們的瀏覽器能夠進行渲染,因此咱們不會看見阻塞的狀況。下面這個 demo 演示了在異步遍歷先後發生的事情:

 總結

  如今,相信你已經認識了 JavaScript 的真實面目了吧。 JavaScript 是一門單線程的語言,可是其事件循環的特性使得咱們能夠異步的執行程序。這些異步的程序也就是一個又一個獨立的任務,這些任務包括了 setTimeout、setInterval、ajax、eventListener 等等。關於事件循環,咱們須要記住如下幾點:

  •   事件隊列嚴格按照時間前後順序將任務壓入執行棧執行;
  •        當執行棧爲空時,瀏覽器會一直不停的檢查事件隊列,若是不爲空,則取出第一個任務;
  •        在每個任務結束以後,瀏覽器會對頁面進行渲染;

  本文 demo 放在 jsfiddle 上,如需轉載,註明下出處就行了。若您發現本文有所紕漏,歡迎在評論區指出。

相關文章
相關標籤/搜索