JavaScript執行緒機制與事件機制

浪裡行舟發表於2019-01-19

一、程式與執行緒

1.程式

程式是指程式的一次執行,它佔有一片獨有的記憶體空間,可以通過windows工作管理員檢視程式(如下圖)。同一個時間裡,同一個計算機系統中允許兩個或兩個以上的程式處於並行狀態,這是多程式。比如電腦同時執行微信,QQ,以及各種瀏覽器等。瀏覽器執行是有些是單程式,如firefox和老版IE,有些是多程式,如chrome和新版IE

2.執行緒

有些程式還不止同時幹一件事,比如Word,它可以同時進行打字、拼寫檢查、列印等事情。在一個程式內部,要同時幹多件事,就需要同時執行多個“子任務”,我們把程式內的這些“子任務”稱為執行緒(Thread)。
執行緒是指CPU的基本排程單位,是程式執行的一個完整流程,是程式內的一個獨立執行單元。多執行緒是指在一個程式內, 同時有多個執行緒執行。瀏覽器執行是多執行緒。比如用瀏覽器一邊下載,一邊聽歌,一邊看視訊。另外我們需要知道JavaScript語言的一大特點就是單執行緒,為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質

由於每個程式至少要幹一件事,所以,一個程式至少有一個執行緒。當然,像Word這種複雜的程式可以有多個執行緒,多個執行緒可以同時執行,多執行緒的執行方式和多程式是一樣的,也是由作業系統在多個執行緒之間快速切換,讓每個執行緒都短暫地交替執行,看起來就像同時執行一樣。當然,真正地同時執行多執行緒需要多核CPU才可能實現。

3.程式與執行緒

  • 應用程式必須執行在某個程式的某個執行緒上
  • 一個程式中至少有一個執行的執行緒: 主執行緒, 程式啟動後自動建立
  • 一個程式中如果同時執行多個執行緒, 那這個程式是多執行緒執行的
  • 一個程式的記憶體空間是共享的,每個執行緒都可以使用這些共享記憶體。
  • 多個程式之間的資料是不能直接共享的

4.單執行緒與多執行緒的優缺點?

單執行緒的優點:順序程式設計簡單易懂

單執行緒的缺點:效率低

多執行緒的優點:能有效提升CPU的利用率

多執行緒的缺點:

  • 建立多執行緒開銷
  • 執行緒間切換開銷
  • 死鎖與狀態同步問題

二、瀏覽器核心

瀏覽器的核心是指支援瀏覽器執行的最核心的程式,分為兩個部分的,一是渲染引擎,另一個是JS引擎。現在JS引擎比較獨立,核心更加傾向於說渲染引擎。

1.不同的瀏覽器可能不太一樣

  • Chrome, Safari: webkit
  • firefox: Gecko
  • IE: Trident
  • 360,搜狗等國內瀏覽器: Trident + webkit

2.核心由很多模組組成

  • html,css文件解析模組 : 負責頁面文字的解析
  • dom/css模組 : 負責dom/css在記憶體中的相關處理
  • 佈局和渲染模組 : 負責頁面的佈局和效果的繪製
  • 定時器模組 : 負責定時器的管理
  • 網路請求模組 : 負責伺服器請求(常規/Ajax)
  • 事件響應模組 : 負責事件的管理

三、定時器引發的思考

1. 定時器真是定時執行的嗎?

我們先來看個例子,試問定時器會保證200ms後執行嗎?

 document.getElementById(`btn`).onclick = function () {
      var start = Date.now()
      console.log(`啟動定時器前...`)
      setTimeout(function () {
        console.log(`定時器執行了`, Date.now() - start)
      }, 200)
      console.log(`啟動定時器後...`)
      // 做一個長時間的工作
      for (var i = 0; i < 1000000000; i++) {
      }
    }


事實上,經過了625ms後定時器才執行。定時器並不能保證真正定時執行,一般會延遲一丁點,也有可能延遲很長時間(比如上面的例子)

2.定時器回撥函式是在分執行緒執行的嗎?

定時器回撥函式在主執行緒執行的, 具體實現方式下文會介紹。

四、瀏覽器的事件迴圈(輪詢)模型

1. 為什麼JavaScript是單執行緒

JavaScript語言的一大特點就是單執行緒,也就是說,同一個時間只能做一件事。那麼,為什麼JavaScript不能有多個執行緒呢?這樣能提高效率啊。

JavaScript的單執行緒,與它的用途有關。作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?

所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

2.Event Loop

JavaScript中所有任務可以分成兩種,一種是同步任務,另一種是非同步任務(如各種瀏覽器事件、定時器和Ajax等)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入”任務佇列”(task queue)的任務,只有”任務佇列”通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行

具體來說,非同步執行的執行機制如下。(同步執行也是如此,因為它可以被視為沒有非同步任務的非同步執行。)

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

(2)主執行緒之外,還存在一個”任務佇列”(task queue)。只要非同步任務有了執行結果,就在”任務佇列”之中放置一個事件。

(3)一旦”執行棧”中的所有同步任務執行完畢,系統就會讀取”任務佇列”,看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步

主執行緒從”任務佇列”中讀取事件,這個過程是迴圈不斷的,所以整個的這種執行機制又稱為Event Loop(事件迴圈)


下面這個例子很好闡釋事件迴圈:

    setTimeout(function () {
      console.log(`timeout 2222`)
      alert(`22222222`)
    }, 2000)
    setTimeout(function () {
      console.log(`timeout 1111`)
      alert(`1111111`)
    }, 1000)
    setTimeout(function () {
      console.log(`timeout() 00000`)
    }, 0)//當指定的值小於 4 毫秒,則增加到 4ms(4ms 是 HTML5 標準指定的,對於 2010 年及之前的瀏覽器則是 10ms)
    function fn() {
      console.log(`fn()`)
    }
    fn()
    console.log(`alert()之前`)
    alert(`------`) //暫停當前主執行緒的執行, 同時暫停計時, 點選確定後, 恢復程式執行和計時
    console.log(`alert()之後`)

有兩點我們需要注意下:

  • 定時器零延遲(setTimeout(func, 0))並不是意味著回撥函式立刻執行。至少4ms,才會執行回撥函式。它取決於主執行緒當前是否空閒與“任務佇列”裡其前面正在等待的任務。
  • 只有在到達指定時間時,定時器就會將相應回撥函式插入“任務佇列”尾部

總結:非同步任務(各種瀏覽器事件、定時器和Ajax等)都是先新增到“任務佇列”(定時器則到達其指定引數時)。當 Stack 棧(JavaScript 主執行緒)為空時,就會讀取 Queue 佇列(任務佇列)的第一個任務(隊首),最後執行

五、H5 Web Workers(多執行緒)

1. Web Workers的作用

正如上面所提到,JavaScript是單執行緒。當一個頁面載入一個複雜運算的 js 檔案時,使用者介面可能會短暫地“凍結”,不能再做其他操作。比如下面這個例子:

<input type="text" placeholder="數值" id="number">
<button id="btn">計算</button>
<script type="text/javascript">
  // 1 1 2 3 5 8    f(n) = f(n-1) + f(n-2)
  function fibonacci(n) {
    return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)  //遞迴呼叫
  }
  var input = document.getElementById(`number`)
  document.getElementById(`btn`).onclick = function () {
    var number = input.value
    var result = fibonacci(number)
    alert(result)
  }
</script>


很顯然遇到這種頁面堵塞情況,很影響使用者體驗的,有沒有啥辦法可以改進這種情形?—-Web Worker就應運而生了!

Web Worker 的作用,就是為 JavaScript 創造多執行緒環境,允許主執行緒建立 Worker 執行緒,將一些任務分配給後者執行。在主執行緒執行的同時,Worker 執行緒在後臺執行,兩者互不干擾。等到 Worker 執行緒完成計算任務,再把結果返回給主執行緒。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 執行緒負擔了,主執行緒(通常負責 UI 互動)就會很流暢,不會被阻塞或拖慢。其原理圖如下:

2. Web Workers的基本使用

主執行緒

  • 首先主執行緒採用new命令,呼叫Worker()建構函式,新建一個 Worker 執行緒
var worker = new Worker(`work.js`);
  • 然後主執行緒呼叫worker.postMessage()方法,向 Worker 發訊息。
  • 接著,主執行緒通過worker.onmessage指定監聽函式,接收子執行緒發回來的訊息。
  var input = document.getElementById(`number`)
  document.getElementById(`btn`).onclick = function () {
    var number = input.value
    //建立一個Worker物件
    var worker = new Worker(`worker.js`)
    // 繫結接收訊息的監聽
    worker.onmessage = function (event) {
      console.log(`主執行緒接收分執行緒返回的資料: `+event.data)
      alert(event.data)
    }
    // 向分執行緒傳送訊息
    worker.postMessage(number)
    console.log(`主執行緒向分執行緒傳送資料: `+number)
  }
    console.log(this) // window

Worker 執行緒

  • Worker 執行緒內部需要有一個監聽函式,監聽message事件。
  • 通過 postMessage(data) 方法來向主執行緒傳送資料。
//worker.js檔案
function fibonacci(n) {
  return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)  //遞迴呼叫
}
console.log(this)//[object DedicatedWorkerGlobalScope]
this.onmessage = function (event) {
  var number = event.data
  console.log(`分執行緒接收到主執行緒傳送的資料: `+number)
  //計算
  var result = fibonacci(number)
  postMessage(result)
  console.log(`分執行緒向主執行緒返回資料: `+result)
  // alert(result)  alert是window的方法, 在分執行緒不能呼叫
  // 分執行緒中的全域性物件不再是window, 所以在分執行緒中不可能更新介面
}

這樣當分執行緒在計算時,使用者介面還可以操作,而且更早拿到計算後資料,響應速度更快了。

3. Web Workers的缺點

  • 不能跨域載入JS
  • worker內程式碼不能訪問DOM(更新UI)
  • 不是每個瀏覽器都支援這個新特性(本文例子只能在Firefox瀏覽器上執行,chrome不支援)

如果需要原始碼,請猛戳Web Workers

如果覺得文章對你有些許幫助,歡迎在我的GitHub部落格點贊和關注,感激不盡!

參考文章

程式和執行緒

程式與執行緒的一個簡單解釋

關於JavaScript單執行緒的一些事

JavaScript 執行機制詳解:再談Event Loop

Web Worker 是什麼鬼?

Web Worker 使用教程

相關文章