本章涵蓋#

  • Web 應用程式生命週期中的各個步驟
  • 處理 HTML 程式碼以產生網頁
  • JavaScript 程式碼的執行順序
  • 透過事件達成互動性
  • Event loop

2.1 生命週期概觀#

典型的用戶端 web 應用程式生命週期從使用者在瀏覽器位址列輸入 URL 或點擊連結開始。整個過程如下:

  1. 使用者輸入 URL 或點擊連結
  2. 瀏覽器產生請求並發送到伺服器
  3. 伺服器處理請求,回傳由 HTML、CSS 和 JavaScript 組成的回應
  4. 瀏覽器處理回應,開始建構頁面

因為用戶端 web 應用程式屬於 GUI 應用程式,其生命週期與其他 GUI 應用程式(如桌面或行動應用)類似,主要分為兩個階段

  • Page building(頁面建構):設置使用者介面
  • Event handling(事件處理):進入迴圈,等待事件發生並呼叫對應的 event handler

當使用者關閉或離開網頁時,應用程式的生命週期結束。

Figure 2.1: 用戶端 Web 應用程式的生命週期

2.2 頁面建構階段(The page-building phase)#

Figure 2.2: 執行 listing 2.1 時的頁面建構過程

在 web 應用程式可以被互動或甚至顯示之前,頁面必須從伺服器回應中的 HTML、CSS 和 JavaScript 建構出來。此階段分為兩個步驟,並可交替進行:

  1. 解析 HTML 並建構 DOM
  2. 執行 JavaScript 程式碼

瀏覽器在處理 HTML 節點時執行步驟 1;當遇到 <script> 元素時切換到步驟 2。這兩個步驟會不斷交替,直到所有 HTML 元素都處理完畢。

2.2.1 解析 HTML 並建構 DOM#

頁面建構從瀏覽器接收 HTML 程式碼開始。瀏覽器逐一解析 HTML 元素,建構 DOM(Document Object Model)——HTML 頁面的結構化表示,每個 HTML 元素都表示為一個節點(node)

DOM 樹的結構特性:

  • 除了根節點(html)外,每個節點恰好有一個父節點(parent)
  • 一個節點可以有任意數量的子節點(children)
  • 同一父節點的子節點互稱為兄弟節點(siblings)

雖然 HTML 與 DOM 密切相關,但它們並非同一件事。應該把 HTML 程式碼視為瀏覽器建構初始 DOM 時所遵循的藍圖(blueprint)。瀏覽器甚至會修正 HTML 中的錯誤——例如將錯置於 <head> 中的 <p> 元素自動移到 <body> 中。

Figure 2.4: 瀏覽器遇到第一個 script 元素時已建構的 DOM 樹

2.2.2 執行 JavaScript 程式碼#

所有 <script> 元素中的 JavaScript 程式碼由瀏覽器的 JavaScript 引擎執行(如 Firefox 的 Spidermonkey、Chrome 與 Opera 的 V8、Edge 的 Chakra)。

Global objects(全域物件)

瀏覽器透過全域物件提供 API,讓 JavaScript 引擎與頁面互動:

  • window:代表頁面所在的視窗,是最頂層的全域物件,所有其他全域物件、全域變數和 browser API 都可透過它存取
  • document:代表當前頁面的 DOM,可用來修改頁面的任何部分——建立或移除節點、修改現有元素等

兩種 JavaScript 程式碼

  • Global code(全域程式碼):位於函式外部的程式碼,由 JavaScript 引擎自動依序執行
  • Function code(函式程式碼):位於函式內部的程式碼,需要被呼叫才會執行(由全域程式碼、其他函式或瀏覽器觸發)

Figure 2.6: 執行 JavaScript 程式碼時的程式執行流程

在頁面建構階段執行 JavaScript

當瀏覽器遇到 <script> 節點時,會暫停 DOM 建構,開始執行 JavaScript 程式碼。執行完畢後,瀏覽器繼續處理剩餘的 HTML 節點。

JavaScript 程式碼只能存取已經建構的 DOM 節點。例如,在第一個 <script> 中無法存取位於其後面的元素,這也是為什麼開發者傾向將 <script> 元素放在頁面底部。

在一個 <script> 元素中建立的全域變數,可以在其他 <script> 元素中正常存取。這是因為全域 window 物件(儲存所有全域 JavaScript 變數)在頁面的整個生命週期中都存在且可存取。

Figure 2.7: 執行 script 元素中 JavaScript 後的 DOM 狀態

2.3 事件處理(Event handling)#

用戶端 web 應用程式是 GUI 應用程式,需要回應不同類型的事件:滑鼠移動、點擊、鍵盤按鍵等。因此,在頁面建構階段執行的 JavaScript 程式碼除了影響全域狀態和修改 DOM 之外,還可以註冊事件監聽器(event listeners / event handlers)

2.3.1 Event-handling overview(事件處理概觀)#

瀏覽器的執行環境基於**單執行緒(single-threaded)**模型——同一時間只能執行一段程式碼。就像銀行只開了一個窗口,所有人必須排隊等候。

由於無法保證使用者總是耐心等待上一個事件處理完才觸發下一個事件,瀏覽器使用**事件佇列(event queue)**來追蹤已發生但尚未處理的事件。

事件處理流程:

  1. 瀏覽器檢查事件佇列的頂端
  2. 若沒有事件,繼續檢查
  3. 若有事件,取出該事件並執行對應的 handler;在此期間,其他事件在佇列中等候

因為一次只能處理一個事件,撰寫需要大量執行時間的 event handler 會導致 web 應用程式變得無回應。

Figure 2.8: 事件處理階段中,所有事件依序從事件佇列中取出處理

事件是非同步的(asynchronous)

事件可在不可預測的時間、以不可預測的順序發生。常見的事件類型:

  • Browser events:頁面載入完成或即將卸載
  • Network events:來自伺服器的回應(Ajax、server-side events)
  • User events:滑鼠點擊、滑鼠移動、鍵盤按鍵
  • Timer events:timeout 到期或 interval 觸發

除了全域程式碼外,頁面上絕大多數的程式碼都是作為某個事件的回應而執行的。

2.3.2 註冊事件處理器(Registering event handlers)#

在用戶端 web 應用程式中,有兩種方式可以註冊事件:

方式一:指派函式到特殊屬性

window.onload = function () {};
document.body.onclick = function () {};

此方式簡單但有缺點:每個事件只能註冊一個 handler,後設定的會覆蓋先前的。

方式二:使用 addEventListener 方法(推薦)

document.body.addEventListener("mousemove", function () {
  var second = document.getElementById("second");
  addMessage(second, "Event: mousemove");
});

document.body.addEventListener("click", function () {
  var second = document.getElementById("second");
  addMessage(second, "Event: click");
});

addEventListener 允許為同一事件註冊多個 handler,是推薦的做法。

Figure 2.9: 事件處理階段範例——處理 mousemove 和 click 事件

2.3.3 處理事件(Handling events)#

事件處理的核心機制:當事件發生時,瀏覽器呼叫對應的 event handler。由於單執行緒模型,同一時間只能執行一個 handler,後續事件必須等待當前 handler 完全執行完畢。

事件處理的實際運作流程:

  1. 使用者操作(如滑鼠移動和點擊)產生的事件,按發生順序被放入 event queue
  2. Event loop 檢查佇列,發現 mousemove 事件在佇列前端,執行對應的 handler
  3. mousemove handler 處理期間,click 事件在佇列中等待
  4. mousemove handler 執行完畢後,event loop 再次檢查佇列,取出並處理 click 事件
  5. Event loop 持續運行,等待新事件,直到使用者關閉 web 應用程式

每次 event handler 執行時,都可能會修改 DOM——例如透過 addMessage 函式建立新的 li 元素並加入到 DOM 中。

Figure 2.10: 範例程式的 DOM 結構

2.4 Summary#

  • 瀏覽器接收的 HTML 程式碼被當作建立 DOM 的藍圖,DOM 是用戶端 web 應用程式結構的內部表示
  • 我們使用 JavaScript 程式碼動態修改 DOM,為 web 應用程式帶來動態行為
  • 用戶端 web 應用程式的執行分為兩個階段:
    • Page building:處理 HTML 以建立 DOM,遇到 <script> 節點時執行全域 JavaScript 程式碼。JavaScript 程式碼可任意修改當前 DOM,並可註冊 event handler
    • Event handling:事件按產生順序逐一處理。Event-handling 階段高度依賴 event queue,所有事件按發生順序儲存在佇列中。Event loop 持續檢查佇列頂端,若有事件則呼叫對應的 handler 函式