iT邦幫忙

2018 iT 邦幫忙鐵人賽
7
Modern Web

重新認識 JavaScript系列 第 36

重新認識 JavaScript 番外篇 (6) - 網頁的生命週期

今天在 Facebook 的前端社團看到有網友問了一個有趣的問題

https://ithelp.ithome.com.tw/upload/images/20180116/20065504BzMWRBSG08.png

看到這個問題,我想大家應該都會很直覺地想到 beforeunload 以及 unload 事件吧。

說到這個 beforeunloadunload 就想來問問各位,使用過前端 MVVM 框架開發的朋友都知道 Component 都有生命週期的觀念,但你知道其實網頁也有生命週期嗎?


DOMContentLoadedload 事件

還記得我們曾在 重新認識 JavaScript: Day 12 透過 DOM API 查找節點 以及 重新認識 JavaScript: Day 16 那些你知道與不知道的事件們 一文中有提到過,當瀏覽器在載入網頁時,瀏覽器會先分析這個 HTML 檔案且「由上而下」依序來讀取解析網頁的內容:

https://d1dwq032kyr03c.cloudfront.net/upload/images/20171219/20065504och2Xekk7T.png

所以當我們嘗試著在 <head> ... </head> 裡面的 <script> 內去存取 DOM 的內容,實際上是無法的,因為此時 DOM 結構尚未形成。

所以此時我們就必須要利用 DOMContentLoadedload 事件,來確保 DOM 結構被完整的讀取跟解析。

document.addEventListener("DOMContentLoaded", function(){
  // DOM Ready!
});

或是

window.addEventListener("load", function(event) {
  // All resources finished loading!
});

兩者的差異在前面也曾提過, load 事件是在網頁「所有」資源都已經載入完成後才會觸發,而 DOMContentLoaded 事件是在 DOM 結構被完整的讀取跟解析後就會被觸發,不須等待外部資源讀取完成。換言之, load 事件會在 DOMContentLoaded 之後才被觸發,而這兩個事件也都可以確保網頁結構載入完成。

看到這裡,也許你會想起 jQuery 的 ready() function。

沒錯,事實上 jQuery.ready() 做的事與 DOMContentLoaded 是一樣的,差別只在於針對老舊瀏覽器的支援程度。 讓我們來看看 jQuery 的原始碼片段:

jQuery.ready.promise = function( obj ) {
  if ( !readyList ) {

    readyList = jQuery.Deferred();

    // Catch cases where $(document).ready() is called after the browser event has already occurred.
    // we once tried to use readyState "interactive" here, but it caused issues like the one
    // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
    if ( document.readyState === "complete" ) {
      // Handle it asynchronously to allow scripts the opportunity to delay ready
      setTimeout( jQuery.ready );

    // Standards-based browsers support DOMContentLoaded
    } else if ( document.addEventListener ) {
      // Use the handy event callback
      document.addEventListener( "DOMContentLoaded", completed, false );

      // A fallback to window.onload, that will always work
      window.addEventListener( "load", completed, false );

    // If IE event model is used
    } else {
      // Ensure firing before onload, maybe late but safe also for iframes
      document.attachEvent( "onreadystatechange", completed );

      // A fallback to window.onload, that will always work
      window.attachEvent( "onload", completed );

      // If IE and not a frame
      // continually check to see if the document is ready
      var top = false;

      try {
        top = window.frameElement == null && document.documentElement;
      } catch(e) {}

      if ( top && top.doScroll ) {
        (function doScrollCheck() {
          if ( !jQuery.isReady ) {

            try {
              // Use the trick by Diego Perini
              // http://javascript.nwbox.com/IEContentLoaded/
              top.doScroll("left");
            } catch(e) {
              return setTimeout( doScrollCheck, 50 );
            }

            // detach all dom ready events
            detach();

            // and execute any waiting functions
            jQuery.ready();
          }
        })();
      }
    }
  }
  return readyList.promise( obj );
};

由於 IE 在 IE8 以前是沒有 DOMContentLoaded 這個事件,所以可以看到 jQuery 為了確保瀏覽器的完整相容性,透過各種不同的方式來實作 ready

如果瀏覽器支援 DOMContentLoaded 的話,就會直接註冊 DOMContentLoaded 事件,並且加上 load 事件來作為 fallback 保險:

else if ( document.addEventListener ) {
  // Use the handy event callback
  document.addEventListener( "DOMContentLoaded", completed, false );

  // A fallback to window.onload, that will always work
  window.addEventListener( "load", completed, false );
}

如果是舊版本的 IE,則透過 attachEvent 加上 onreadystatechangeonload 來確保相容性:

// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", completed );

// A fallback to window.onload, that will always work
window.attachEvent( "onload", completed );

另外,由於判斷 document.readyStatereadyStateChange 的時機點會有誤差,所以 jQuery 利用了不斷執行 doScrollCheck() 來檢查 DOM 是否確實載入完成。


beforeunloadunload

講完 load 的部分之後,再回到這篇一開始講的 beforeunloadunload

當使用者嘗試要關閉網頁、點擊了網頁的連結,或是要往上/下一頁、甚至重新整理頁面的時候,就會觸發這類事件。

兩者的差別在於, beforeunload 是在網頁被卸載「之前」觸發,而 unload 是在網頁被卸載「之後」觸發,所以如果我們想要跳出警告視窗提醒使用者是否離開,就得在 beforeunload 事件處理,而不是 unload ,因為此時網頁已經離開。

https://ithelp.ithome.com.tw/upload/images/20171219/20065504Ap3UvTrII8.png

值得一提的是,過去我們在 beforeunload 事件可以自訂提示訊息,這個功能在 Chrome v51 (2016/04) 時被取消了,理由是防止 beforeunload 的自訂訊息被用來做為詐騙用途。 詳情請見 Remove custom messages in onbeforeunload dialogs


回歸主題,所以網頁的生命週期,若是以「事件」來區分,大致上可以分成幾個部分:

  1. DOMContentLoaded
  2. Load
  3. Beforeunload
  4. Unload

如果是 document.readyState 階段來區分,則可以分成

  1. "loading"
  2. "interactive" (相當於 DOMContentLoaded)
  3. "complete" (相當於 Load)

最後,回到一開始網友的問題,若想要在「點擊瀏覽器的關閉才觸發的事件」在實務上是不可能的,因為當使用者嘗試要關閉網頁、點擊了網頁的連結,或是要往上/下一頁、甚至重新整理頁面的時候,都會觸發 beforeunload

但若是只想要避開點擊網頁連結的話,則是可以在 <a>click 事件加上 window.onbeforeunload = null; 來取消 beforeunload,警告視窗就不會跳出來煩人了。


上一篇
重新認識 JavaScript 番外篇 (5) - 鐵人賽戰況分析 II
下一篇
重新認識 JavaScript 番外篇 (7) - 判斷式 (a == 1 && a == 2 && a == 3) 結果為 true ?
系列文
重新認識 JavaScript37

1 則留言

0
he.chun
iT邦新手 5 級 ‧ 2019-01-28 00:09:43

還沒看完先給讚!!!/images/emoticon/emoticon81.gif

我要留言

立即登入留言