添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
慷慨的黄豆  ·  C++ STL std::list探索 - 知乎·  1 年前    · 
悲伤的鸭蛋  ·  Nginx ...·  1 年前    · 

本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。

購書連結 https://www.tenlong.com.tw/products/9789864344130

讓我們再次重新認識 JavaScript!

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

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

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

DOMContentLoaded load 事件

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

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

所以此時我們就必須要利用 DOMContentLoaded load 事件,來確保 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 ,因為此時網頁已經離開。

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

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

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

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

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