JS的事件對象與事件機制

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

1. JS基礎知識深刻總結
2. 對象高級
3. 函數高級
4. 事件對象與事件機制css

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

事件對象與事件機制

4.1 同步與異步

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

異步(Asynchronous):你在作一件事情,這件事可能會耗時好久,而此時你能夠在等待的過程當中,去作另一件事。ajax

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

而你以爲這過程耗時蠻久,能夠先去作其它事情,好比去掃地,直到水沸騰。這就是異步。windows

4.1 線程與進程

1.進程(process): 程序的一次執行, 它佔有一片獨有的內存空間。能夠經過windows任務管理器查看進程。進程負責爲程序的運行提供必備的環境,至關於工廠的車間。跨域

2.線程(thread): 是進程內的一個獨立執行單元。 是CPU的最小的調度單元。 是程序執行的一個完整流程。線程負責執行進程中的程序,至關於工廠工做的工人。promise

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);
}

// 一、點擊 Button 4,會在控制檯輸出什麼? 5
/*An:無論點擊哪一個button都是輸出5.*/
// 2. 給出一種預期的實現方式
/*  將for循環中的var 變成 let 或者 用對象.屬性保存起來i的值 */
</script>
複製代碼

此文檔爲呂涯原創,可任意轉載,但請保留原連接,標明出處。 文章只在CSDN和掘金第一時間發佈: CSDN主頁:https://blog.csdn.net/LY_code 掘金主頁:https://juejin.im/user/5b220d93e51d4558e03cb948 如有錯誤,及時提出,一塊兒學習,共同進步。謝謝。 😝😝😝

相關文章
相關標籤/搜索