JS的事件物件與事件機制

Fengmaybe發表於2019-03-04

本系列將從以下專題去總結:

1. JS基礎知識深入總結
2. 物件高階
3. 函式高階
4. 事件物件與事件機制

暫時會對以上四個專題去總結,現在開始Part4: 事件物件與事件機制。下圖是我這篇的大綱。

事件物件與事件機制

4.1 同步與非同步

同步(Synchronous):你在做一件事情,不能同時去做另外一件事。

非同步(Asynchronous):你在做一件事情,這件事可能會耗時很久,而此時你可以在等待的過程中,去做另外一件事。

比如煮開水這件事吧...在這過程,你擔心水沸了而不去做其它事情,就等到水沸騰,那就是同步。

而你覺得這過程耗時蠻久,可以先去做其它事情,比如去掃地,直到水沸騰。這就是非同步。

4.1 執行緒與程式

1.程式(process): 程式的一次執行, 它佔有一片獨有的記憶體空間。可以通過windows工作管理員檢視程式。程式負責為程式的執行提供必備的環境,相當於工廠的車間。

2.執行緒(thread): 是程式內的一個獨立執行單元。 是CPU的最小的排程單元。 是程式執行的一個完整流程。執行緒負責執行程式中的程式,相當於工廠工作的工人。

3.圖解程式、執行緒和程式的關係:

程式執行緒程式的關係
一個程式A有多個程式,那麼程式A 就是多程式的程式。程式B是隻有一個程式,那麼程式B就是單程式的程式。 如果一個程式有一個執行緒,那麼這個程式就是單執行緒的。如果一個程式有多個執行緒,那麼這個程式就是多執行緒的。單和多 執行緒是針對程式而言的。比如,我一個程式有兩個程式,這兩個程式分別有一個執行緒,那麼這個程式還是單執行緒的程式。

4.程式與執行緒

  • 應用程式必須執行在某個程式的某個執行緒上
  • 一個程式中至少有一個執行的執行緒: 主執行緒。程式啟動後自動建立
  • 一個程式中也可以同時執行多個執行緒, 我們會說程式是多執行緒執行的
  • 一個程式內的資料可以供其中的多個執行緒直接共享
  • 多個程式之間的資料是不能直接共享的(因為程式是分配獨立的記憶體空間給它)
  • 執行緒池(thread pool): 儲存多個執行緒物件的容器, 實現執行緒物件的反覆利用。

5.何為多程式與多執行緒?

  • 多程式執行: 一個應用程式可以同時啟動多個例項執行。
  • 多執行緒: 在一個程式內, 同時有多個執行緒執行

6.比較單執行緒與多執行緒?

單執行緒與多執行緒
7.JS是單執行緒還是多執行緒?

  • JS是單執行緒執行的

    在JS設計的本意只是對一些簡單的操作而已,比如提交表單使用者名稱和密碼之類的。當沒有JS時,那麼這些資料就會提交到伺服器中,那麼這個資料處理將會特別大,首先假設有1000個人同時註冊,那麼這些請求就會到伺服器上,伺服器的載入量就會很大,而且,使用者體驗也不好,可能會延遲返回請求資訊。這是如果這些操作在瀏覽器端來操作,那麼就會簡單很多。所以,JS當時設計的初衷也就單執行緒了,因為不需要太多的操作。單執行緒足矣應付,而且不佔太多的記憶體。當然後面會說道,因為他的功能(DOM操作等)也決定了它只能單執行緒。

  • 但使用H5中的 Web Workers可以多執行緒執行(主執行緒只有一個,要做其他的事可以啟動分執行緒)

8.瀏覽器執行是單程式還是多程式?

  • 有的是單程式
    • Firefox(據Mozilla方面表示,FireFox 54版瀏覽器已經可以將全部開啟的網頁標籤分為最多四個程式來執行,以此提升瀏覽器對PC硬體的利用率。)
    • 老版IE
  • 有的是多程式
    • chrome
    • 新版IE

9.如何檢視瀏覽器是否是多程式執行的呢 ?

  • 工作管理員==>程式

10.瀏覽器執行是單執行緒還是多執行緒?

  • 都是多執行緒執行的

4.2 瀏覽器核心(Browser core)

瀏覽器核心:支撐瀏覽器執行的最核心的程式。

4.2.1 不同的瀏覽器可能有不同的核心

  • Chrome, Safari : webkit核心
  • Firefox : Gecko核心
  • IE : Trident核心
  • 360,搜狗等國內瀏覽器: Trident + webkit(雙核,嘻嘻,給你一個眼神~)

4.2.2 瀏覽器核心由很多模組組成

  • 主執行緒

    • JS引擎模組 : 負責js程式的編譯與執行(也是程式碼,是解釋我們寫的程式碼)
    • HTML,CSS文件解析模組 : 負責頁面文字的解析(一開始是HTML和CSS文字資訊)
    • DOM/CSS模組 : 負責dom/css在記憶體中的相關處理 (把一些標籤轉為DOM樹物件)
    • 佈局和渲染模組 : 負責頁面的佈局和效果的繪製(參照記憶體中的物件資料進行佈局與渲染)
  • 分執行緒

    • 定時器模組 : 負責定時器的管理
    • DOM事件模組 : 負責事件的管理(onclick..)
    • 網路請求模組 : 負責Ajax請求
      瀏覽器核心組成

4.3 JS執行緒

1.如何證明js執行是單執行緒的?

  • setTimeout()的回撥函式是在主執行緒執行的
  • 定時器回撥函式只有在執行棧中的程式碼全部執行完後才有可能執行

2.為什麼js要用單執行緒模式, 而不用多執行緒模式?

  • JavaScript的單執行緒,與它的用途有關。
  • 作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM。
  • 如果在一個p物件(p標籤),假設是在多執行緒的環境下,那麼就會有執行緒的切換,當一個是操作修改p的內容,另一個是刪除p標籤,這個時候就會有衝突。
  • 這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。

3.程式碼的分類:

  • 初始化程式碼(同步程式碼)
  • 回撥程式碼(非同步程式碼)

4.js引擎執行程式碼的基本流程

  • 先執行初始化程式碼:包含一些特別的程式碼
    • 設定定時器
    • 繫結事件監聽
    • 傳送ajax請求
  • 後面在某個時刻才會執行回撥程式碼:回撥函式(非同步執行) 看個案例:
<script type="text/javascript">
  setTimeout(function () {
    console.log('timeout 2222')
  }, 2000)
  setTimeout(function () {
    console.log('timeout 1111')
  }, 1000)
  
  function fn() {
    console.log('fn()')
  }
  fn()
  console.log('alert()之前')
  alert('------') //暫停當前主執行緒的執行, 同時暫停定時器的計時, 點選確定後, 恢復程式執行和計時。
  console.log('alert()之後')
</script>
複製程式碼

5.js是單執行緒執行的(回撥函式也是在主執行緒)
6.H5提出了實現多執行緒的方案: Web Workers
7.只能是主執行緒更新介面

4.4 定時器引出的問題

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

  • 定時器並不能保證真正定時執行
  • 一般會延遲一丁點(可以接受), 也有可能延遲很長時間(不能接受)

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

  • 在主執行緒執行的, js是單執行緒的。不管是回撥函式還是非回撥函式都是在主執行緒執行的。

3.定時器是如何實現的?

  • 事件迴圈模型(後面講)
<button id="btn">啟動定時器</button>
<script type="text/javascript">
  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++) {
    }
  }
</script>
複製程式碼

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

4.5.1 一些簡述

  • 程式碼分類
    • 初始化執行程式碼:一些同步的程式碼, 包含繫結dom事件監聽, 設定定時器, 傳送ajax請求的程式碼
    • 回撥執行程式碼: 處理回撥邏輯,非同步的程式碼(繫結dom事件監聽, 設定定時器, 傳送ajax請求的各自的回撥函式)
  • js引擎執行程式碼的基本流程:
    • 先執行初始化程式碼===>後執行回撥程式碼
  • 模型的2個重要組成部分:
    • 事件(定時器/DOM事件/AJAX)管理模組
    • 回撥佇列:等待去處理的回撥函式。
  • 模型的運轉流程
    • 執行初始化程式碼, 將事件回撥函式交給對應模組管理
    • 當事件發生時, 管理模組會將回撥函式及其資料新增到回撥列隊中
    • 只有當初始化程式碼執行完後(可能要一定時間), 才會遍歷讀取回撥佇列中的回撥函式執行

4.5.2 模型原理圖

以下這張圖就是event-driven interaction model(事件驅動模型)。

另外簡單的說一下request-response model(事件響應模型),這個就相當於瀏覽器去伺服器請求一些資料,伺服器接收到這些請求,去處理這些請求,緊接著返回給瀏覽器的請求資料,瀏覽器接收到資料解析到頁面上的一個過程。

現在我們主要是看一下event-driven interaction model:

事件迴圈模型

首先,這個圖分為三個部分:JS引擎等主執行緒、瀏覽器核心的分執行緒、任務佇列。

在第一部分中:(堆記憶體和棧記憶體)

  • 執行棧(execution stack)
    • 所有的程式碼都是在此空間中執行的
    • 各個任務按照文件定義的順序一一推入"執行棧"中,當前一個任務執行完畢,才會開始執行下一個任務。
    • 實則是把上下文物件壓棧和彈出,這裡執行了一些初始化程式碼,包含繫結dom事件監聽, 設定定時器, 傳送ajax請求的程式碼。
    • 事件(定時器/DOM事件/AJAX)回撥函式交給對應模組管理。用setTimeout做比較,他會把回撥函式和延遲時間1000給WebAPIS的SetTimeout模組處理。這部分並不是在主程式中執行的,而是在瀏覽器分執行緒中執行。
  • heap 用來儲存宣告的物件。

在第二部分中: 這一塊主要是交給瀏覽器的分執行緒處理。以setTimeout定時器為比較,他會拿到回撥函式和延遲時間1000,當延遲時間過了之後,就會把回撥函式推入佇列中。

在第三部分中:

  • 臨時儲存著回撥函式,當執行棧為空時,就會依次將其回撥函式壓入執行棧中。

  • 這個部分叫做callback queue。也叫任務佇列(task queue)、訊息佇列(message queue)、事件佇列(event queue)。指的都是同一個。

剛剛以定時器介紹了這個過程。我們再以AJAX為例看看是如何執行這些過程的?

AJAX執行緒和主執行緒

上圖以AJAX非同步請求為例,發起非同步任務後,由AJAX執行緒執行耗時的非同步操作,而JS引擎執行緒繼續執行堆中的其他同步任務,直到堆中的所有非同步任務執行完畢。然後,從訊息佇列中依次按照順序取出訊息作為一個同步任務在JS引擎執行緒中執行,那麼AJAX的回撥函式就會在某一時刻被呼叫執行。

另外一點,我們看到事件機制模型圖有事件輪詢(event loop),就是從任務佇列中迴圈取出回撥函式放入執行棧中處理(一個接一個)。JS引擎執行緒用來執行棧中的同步任務(初始化程式碼),當所有同步任務(初始化程式碼)執行完畢後,棧被清空,然後讀取訊息佇列中的一個待處理任務,並把相關回撥函式壓入棧中,單執行緒開始執行新的同步任務。JS引擎執行緒從訊息佇列中讀取任務是不斷迴圈的,每次棧被清空後,都會在訊息佇列中讀取新的任務,如果沒有新的任務,就會等待,直到有新的任務,這就叫事件輪詢。

4.5.3 巨集任務和微任務、

巨集任務佇列

  1. 巨集任務佇列可以有多個
  2. setTimeout
  3. 當巨集任務佇列執行完畢後,此刻微任務佇列中有任務,會立即執行微任務佇列中的所有任務

微任務佇列

  1. 微任務佇列只有一個
  2. promise物件的成功的回撥和progress.nextTick()
  3. 會再次執行新的巨集任務佇列(如果有)
function fun() {
  console.log('程式開始執行', 11111111);
  setTimeout(function () {
    console.log('定時器開始執行',666666);
  }, 0)
  new Promise(function (resolve, reject) {
    console.log('promise物件開始執行', 2222222);
    for (var i = 0; i < 5; i++) {
      console.log(i, 33333333);
    }
    resolve();
  })
    .then(() => {
      console.log('promise物件成功的回撥執行', 555555);
    })
.then(() => {
    console.log('promise物件失敗的回撥執行', 555555);
  });
  console.log('程式執行完畢',  444444444444);
}
fun();
//以上程式執行順序結構就是上述的數字123456.
複製程式碼

巨集任務和微任務

4.6 H5 Web Workers

4.6.1 介紹

Web Workers 是 HTML5 提供的一個javascript多執行緒解決方案,我們可以將一些大計算量的程式碼交由web Worker執行而不凍結使用者介面,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

4.6.2 案例引入

實現一個斐波那契數列,在頁面input中輸入數列項的值,得到相應的數列值。

<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>
複製程式碼

以上操作會在js引擎的主執行緒中,在計算的過程中,會凍結使用者介面,達到不佳的使用者體驗。

4.6.3 使用Web Workers

  • H5規範提供了js分執行緒的實現,取名為: Web Workers。它支援JavaScript多執行緒的操作。

  • 相關API

    • Worker: 建構函式, 載入分執行緒執行的js檔案
    • Worker.prototype.onmessage: 用於接收另一個執行緒的回撥函式
    • Worker.prototype.postMessage: 向另一個執行緒傳送訊息
  • 使用步驟 步驟1:建立在分執行緒執行的js檔案

  //workers.js檔案

  function fibonacci(n) {
    return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)  //遞迴呼叫
  }

  console.log(this)
  //當接受到主執行緒的資料時
  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, 所以在分執行緒中不可能更新介面
  }
複製程式碼

步驟2:在主執行緒中的js中發訊息並設定回撥

//主執行緒

  <input type="text" placeholder="數值" id="number">
  <button id="btn">計算</button>
  <script type="text/javascript">
    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

  </script>
複製程式碼

回顧4.6.2的案例引入,我們可知,那個是完全在主執行緒中操作,帶來的弊端就是凍結了使用者介面。而使用Workers在分執行緒中處理耗時的運算,在主執行緒去接受計算好的資料,就可以解決直接使用主執行緒的凍結使用者介面的弊端,這個時候不會凍結使用者介面,但是子執行緒完全受主執行緒控制,且子執行緒不得操作DOM,因為其this不是window

4.6.4 圖解

H5 Web Workers(多執行緒)

4.6.5 不足點

  1. 慢(本來在主執行緒肯定是更快的,現在在分執行緒肯定會慢點,是指這個層面上的"慢")
  2. 不能跨域載入JS
  3. worker內程式碼不能訪問DOM(更新UI)(因為分執行緒的this不是window)
  4. 不是每個瀏覽器都支援這個新特性

4.7 習題與案例

案例1

console.log("1");

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

console.log("3");

setTimeout(function(){
	console.log("4");
},0);
複製程式碼

輸出結果: 1->3->4->2.

案例1分析

  1. 兩個console.log()都是同步,按照文件的順序將它們推入"執行棧"中。
  2. 執行棧中的同步任務執行完畢。
  3. 將兩個非同步任務(定時器)按照第二個引數 (延遲執行的時間) 順序推入"任務佇列"中。
  4. 執行非同步任務。

案例2

//同步code1
var t = true;

//非同步code2
window.setTimeout(function (){
    t = false;
},1000);

//同步code2
while (t){}

//同步code3
alert('end');
複製程式碼

案例2分析

  1. 先執行同步code1 -> 同步code2
  2. 此時到進行同步code2while(true){},進入死迴圈,說明這個時候棧中的同步程式碼永遠不會執行完,也就棧永遠不會清空出來,那麼任務佇列中的程式碼就不會執行。也就是任務佇列中的非同步的程式碼就無法執行。

案例3

//只有使用者觸發點選事件才會被推入佇列中(如果點選時間小於定時器指定的時間,則先於定時器推入,否則反之)
document.querySelector("#box").onclick = function(){
  console.log("click");
};
//第一個推入佇列中
setTimeout(function(){
  console.log("1");
},0);
//第三個推入佇列中
setTimeout(function(){
 console.log("2");
},1000);
//第二個推入佇列中
setTimeout(function(){
  console.log("3");
},0);
複製程式碼

執行結果:如上面程式碼段中的分析。

案例3分析:

以上都是非同步程式碼,包括onclick那個。一定要分清哪些是非同步的程式碼。非同步程式碼中的回撥函式都會定義在heap中,也就是在右邊的堆分配一塊記憶體給他們,這個時候根據他們指定的時候結束後,把他們的回撥函式放到任務佇列等待執行。

setTimeout的作用是在間隔一定的時間後,將回撥函式插入訊息佇列中,等棧中的同步任務都執行完畢後,再執行。因為棧中的同步任務也會耗時,所以間隔的時間一般會大於等於指定的時間(指定的時間就是回撥函式後面一個引數的毫秒值)。

setTimeout(fn, 0)的意思是,將回撥函式fn立刻插入訊息佇列,等待執行,而不是立即執行。只有等待同步任務全部執行完,然後js引擎(js虛擬機器)就去從任務佇列中拿出來去執行。

案例4

setTimeout(function() {
    console.log("a")
}, 0)

for(let i=0; i<10000; i++) {}
console.log("b")
複製程式碼

執行結果:先輸出b 再輸出a

案例4分析:

這個與案例3就差不多了。先執行for迴圈的同步程式碼。定時器是非同步程式碼,先等執行緒的同步程式碼執行結束後在從任務佇列中去拿這些非同步程式碼段執行。

案例5
執行下面這段程式碼,執行後,在 5s 內點選兩下,過一段時間(>5s)後,再點選兩下,整個過程的輸出結果是什麼?

//非同步程式碼
setTimeout(function(){
    for(var i = 0; i < 100000000; i++){}
    console.log('timer a');
}, 0)
//同步程式碼
for(var j = 0; j < 5; j++){
    console.log(j);
}
//非同步程式碼
setTimeout(function(){
    console.log('timer b');
}, 0)
//函式
function waitFiveSeconds(){
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('finished waiting');
}
//非同步程式碼
document.addEventListener('click', function(){
    console.log('click');
})
//同步程式碼
console.log('click begin');
//同步程式碼,呼叫函式,執行函式體
waitFiveSeconds();
複製程式碼

案例5分析:
首先,先執行同步任務。其中waitFiveSeconds是耗時操作,持續執行長達5s。

0
1
2
3
4
click begin
finished waiting
複製程式碼

然後,在JS引擎執行緒執行的時候,timer a對應的定時器產生的回撥、 timer b對應的定時器產生的回撥和兩次click對應的回撥被先後放入訊息佇列。由於JS引擎執行緒空閒後,會先檢視是否有事件可執行,接著再處理其他非同步任務。因此會產生 下面的輸出順序。

click
click
timer a
timer b
複製程式碼

最後,5s 後的兩次 click 事件被放入訊息佇列,由於此時JS引擎執行緒空閒,便被立即執行了。

click
click
複製程式碼

案例6

<script>
for (var i = 0; i < 5; i++){
    var btn = document.createElement('button');
    btn.appendChild(document.createTextNode('Button ' + i));
    btn.addEventListener('click', function (){
        console.log(i);
    });
    document.body.appendChild(btn);
}

// 1、點選 Button 4,會在控制檯輸出什麼? 5
/*An:不管點選哪個button都是輸出5.*/
// 2. 給出一種預期的實現方式
/*  將for迴圈中的var 變成 let 或者 用物件.屬性儲存起來i的值 */
</script>
複製程式碼

JS的事件物件與事件機制
此文件為呂涯原創,可任意轉載,但請保留原連結,標明出處。
文章只在CSDN和掘金第一時間釋出:
CSDN主頁:https://blog.csdn.net/LY_code
掘金主頁:https://juejin.im/user/5b220d93e51d4558e03cb948
若有錯誤,及時提出,一起學習,共同進步。謝謝。 ???

相關文章