深入理解JavaScript運行機制

深入理解JavaScript運行機制

前言

  • 本文是寫作在給團隊新人培訓之際,所以其實本文的受衆是對JavaScript的運行機制不瞭解或瞭解起來有困難的小夥伴。也就是說,其實真正的原理和本文闡述的並不完全符合,就如中學課本和大學課本一樣,大學老師會告訴你高中的一些東西是在某些理想情況下得到的結論,本文同理。
  • 本文的目的是希望大家閱讀之後能對JavaScript的運行機制有一個比較直觀比較快的認識,但更重要的是自己動手實踐,只有實踐才能真正發現問題和得到提升:)
  • 收到了大家的支持和反饋,非常感謝:)

想要理解JavaScript的運行機制,需要分別深刻理解以下幾個點:

  • JavaScript的單線程機制
  • 任務隊列(同步任務和異步任務)
  • 事件和回調函數
  • 定時器
  • Event Loop(事件循環)

JavaScript的單線程機制

JavaScript的一個語言特性(也是這門語言的核心)就是單線程。什麼是單線程呢?簡單地說就是同一時間只能做一件事,當有多個任務時,只能按照一個順序一個完成了再執行下一個。

JavaScript的單線程與它的語言用途是有關的。作爲一門瀏覽器腳本語言,JavaScript的主要用途是完成用戶交互、操作DOM。這就決定了它只能是單線程,否則會導致複雜的同步問題。

設想JavaScript同時有兩個線程,一個線程需要在某個DOM節點上添加內容,而另一個線程的操作是刪除了這個節點,那麼瀏覽器應該以誰爲準呢?

所以爲了避免複雜性,JavaScript從誕生起就是單線程。

爲了提高CPU的利用率,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以這個標準並沒有改變JavaScript單線程的本質。

任務隊列

一個接一個地完成任務也就意味着待完成的任務是需要排隊的,那麼爲什麼會需要排隊呢?

通常排隊有以下兩種原因:

  • 任務計算量過大,CPU處於忙碌狀態;
  • 任務所需的東西爲準備好所以無法繼續執行,導致CPU閒置,等待輸入輸出設備(I/O設備)。> 比如有的任務你需要Ajax獲取到數據才能往下執行

由此JavaScript的設計者也意識到,這時完全可以先運行後面已經就緒的任務來提高運行效率,也就是把等待中的任務先掛起放到一邊,等得到需要的東西再執行。就好比接電話時對方離開了一下,這時正好有另一個來電,於是你便把當前通話掛起,等那個通話結束後,再連回之前的通話。

所以也就出現了同步和異步的概念,任務也被分成了兩種,一種是同步任務(Synchronous),另一種是異步任務(Asynchronous)。

  • 同步任務:需要執行的任務在主線程上排隊,一個接一個,前一個完成了再執行下一個
  • 異步任務:沒有馬上被執行但需要執行的任務,存放在“任務隊列”(task queue)中,“任務隊列”會通知主線程什麼時候哪個異步任務可以執行,然後這個任務就會進入主線程並被執行。> 所有的同步執行都可以看作是沒有異步任務的異步執行

具體來說,異步執行如下:

  • 所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。

    也就是所有能被馬上執行的任務都在主線程上排好了隊,一個接一個的被執行。

  • 主線程之外,還存在一個“任務隊列”(task queue)。只要異步任務有了運行結果,就在“任務隊列”之中放置一個事件。

    也就是說每個異步任務準備好了就會立一個唯一的flag,這個flag用來標識對應的異步任務。

  • 一旦“執行棧”中的所有同步任務執行完畢,系統就會讀取“任務隊列”,看看裏面有哪些事件。那些對應的異步任務,就結束等待裝袋,進入執行棧開始被執行。

    也就是主線程把之前的任務做完了之後,就會來看“任務隊列”中的flag,來把對應的異步任務打包來執行。

  • 主線程不斷重複以上三步。

    只要主線程空了,就會去讀取“任務隊列”。這個過程會被不斷重複,這就是JavaScript的運行機制。

事件和回調函數

事件

“任務隊列”是一個事件的隊列(也可以理解成是消息的隊列),IO設備完成一項任務,就會在“任務隊列”中添加一個時間,表示相關的異步任務可以進入“執行棧”。接着主線程讀取“任務隊列”,查看裏面有哪些事件。

“任務隊列”中的事件,除了IO設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入“任務隊列”,等待主線程讀取。

回調函數

所謂“回調函數”(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。

“任務隊列”是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,“任務隊列”上第一位的事件就自動進入主線程。但是,如果包含“定時器”,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。

Event Loop

主線程從“任務隊列”中讀取事件,這個過程是循環不斷的,所以整個的運行機制又稱爲“Event Loop”(事件循環)

爲了更好地理解Event Loop,下面參照Philip Roberts的演講中的一張圖。

Event Loop

上圖中,主線程在運行時,產生了heap(堆)和stack(棧),棧中的代碼調用各種外部API,並在“任務隊列”中加入各種事件(click,load,done)。當棧中的代碼執行完畢,主線程就會讀取“任務隊列”,並依次執行那些事件所對應的回調函數。

執行棧中的代碼(同步任務),總是在讀取“任務隊列”(異步任務)之前執行。

var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();

上面的代碼中的req.send方法是Ajax操作向服務器發送數據,它是一個異步任務,意味着只有當前腳本的所有代碼執行完,系統纔會去讀取“任務隊列”。所以,它與以下的寫法是等價的。

var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};

也就是說,指定回調函數的部分(onload和onerror),在send()方法的前面或後面是無關緊要的,因爲它們屬於執行棧的一部分,系統總是執行完它們纔會去讀取“任務隊列”。

定時器

除了放置異步任務的事件,“任務隊列”還可以放置定時事件,即指定某些代碼在多少時間之後執行。這叫做定時器(timer)功能,也就是定時執行的代碼。

SetTimeout()setInterval()可以用來註冊在指定時間之後單次或重複調用的函數,它們的內部運行機制完全一樣,區別在於前者指定的代碼是一次性執行,後者會在指定毫秒數的間隔裏重複調用:

setInterval(updateClock, 60000); //60秒調用一次updateClock()

因爲它們都是客戶端JavaScript中重要的全局函數,所以定義爲Window對象的方法。

但作爲通用函數,其實不會對窗口做什麼事情。

Window對象的setTImeout()方法用來實現一個函數在指定的毫秒數之後運行。所以它接受兩個參數,第一個是回調函數,第二個是推遲執行的毫秒數。 setTimeout()setInterval()返回一個值,這個值可以傳遞給clearTimeout()用於取消這個函數的執行。

console.log(1);
setTimeout(function(){console.log(2);}, 1000);
console.log(3);

上面代碼的執行結果是1,3,2,因爲setTimeout()將第二行推遲到1000毫秒之後執行。

如果將setTimeout()的第二個參數設爲0,就表示當前代碼執行完(執行棧清空)以後,立即執行(0毫秒間隔)指定的回調函數。

setTimeout(function(){console.log(1);}, 0);
console.log(2)

上面代碼的執行結果總是2,1,因爲只有在執行完第二行以後,系統纔會執行“任務隊列”中的回調函數。

總之,setTimeout(fn,o)的含義是,指定某個任務在主線程最早可得的空閒時間執行,也就是儘可能早地執行。它在“任務隊列”的尾部添加一個事件,因此要等到同步任務和“任務隊列”現有的事件都處理完,纔會的到執行。

HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,如果低於這個值,就會自動增加。

需要注意的是,setTimeout()只是將事件插入了“任務隊列”,必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等很久,所以並沒有辦法保證回調函數一定會在setTimeout()指定的時間執行。

由於歷史原因,setTimeout()setInterval()的第一個參數可以作爲字符串傳入。如果這麼做,那這個字符串會在指定的超時時間或間隔之後進行求值(相當於執行eval())。

關於深入理解定時器的工作原理,這裏推薦閱讀jQuery的作者John Resig的一篇文章: http://ejohn.org/blog/how-javascript-timers-work/

參考阮一峯老師的博文 http://www.ruanyifeng.com/blog/2014/10/event-loop.html

參考《JavaScript權威指南》

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