自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何監(jiān)聽頁面 DOM 變動并高效響應

開發(fā) 前端
最近在做 chrome 插件開發(fā),既然是插件那就難免不對現(xiàn)有頁面做一些控制,比如事件監(jiān)聽、調整布局、對 DOM 元素的增刪改查等等。其中有一個需求比較有意思,便整理一下順便把涉及到的知識點復習一遍。

最近在做 chrome 插件開發(fā),既然是插件那就難免不對現(xiàn)有頁面做一些控制,比如事件監(jiān)聽、調整布局、對 DOM 元素的增刪改查等等。其中有一個需求比較有意思,便整理一下順便把涉及到的知識點復習一遍。

需求是這樣的:在一個包含懶加載資源以及動態(tài) DOM 元素生成的頁面中,需要針對頁面中存在的元素添加屬性顯示標簽。

從 DOM 變動事件監(jiān)聽說起

首先假設大家已經(jīng)知道 JavaScript 中事件的發(fā)生階段(捕獲-***-冒泡),附上一張圖帶過這個內(nèi)容,我們直接進入尋找解決方法的過程。

 

Graphical representation of an event dispatched in a DOM tree using the DOM event flow

開始的時候我一直在 window 狀態(tài)改變涉及到的事件中尋找,一圈搜尋下來發(fā)現(xiàn)也就 onload 事件最接近了,所以我們看看 MDN 對該事件的定義:

The load event is fired when a resource and its dependent resources have finished loading.

怎么理解資源及其依賴資源已加載完畢呢?簡單來說,如果一個頁面涉及到圖片資源,那么 onload 事件會在頁面完全載入(包括圖片、css文件等等)后觸發(fā)。一個簡單的監(jiān)聽事件用 JavaScript 應該這樣書寫(注意不同環(huán)境下 load 和 onload 的差異):

  1. <script> 
  2.  
  3.   window.addEventListener("load"function(event) { 
  4.  
  5.     console.log("All resources finished loading!"); 
  6.  
  7.   }); 
  8.  
  9.    
  10.  
  11.   // or 
  12.  
  13.   window.onload=function(){ 
  14.  
  15.     console.log("All resources finished loading!"); 
  16.  
  17.   }; 
  18.  
  19.    
  20.  
  21.   // HTML 
  22.  
  23. < body onload="SomeJavaScriptCode"
  24.  
  25.    
  26.  
  27.   // jQuery 
  28.  
  29.   $( window ).on"load", handler ) 
  30.  
  31. </script>  

當然,說到 onload 事件,有一個 jQuery 中相似的事件一定會被提及—— ready 事件。jQuery 中這樣定義這個事件:

Specify a function to execute when the DOM is fully loaded.

需要知道的是 jQuery 定義的 ready 事件實質上是為 DOMContentLoaded 事件設計的,所以當我們談論加載時應該區(qū)分的事件其實是 onload(接口 UIEvent) 以及 DOMContentLoaded(接口 Event),MDN 這樣描述 DOMContentLoaded:

當初始HTML文檔被完全加載和解析時,DOMContentLoaded 事件被觸發(fā),而無需等待樣式表、圖像和子框架完成加載。另一個不同的事件 load 應該僅用于檢測一個完全加載的頁面。

所以可以知道,當一個頁面加載時應先觸發(fā) DOMContentLoaded 然后才是 onload. 類似的事件及區(qū)別包括以下幾類:

  • DOMContentLoaded: 當初始HTML文檔被完全加載和解析時,DOMContentLoaded 事件被觸發(fā),而無需等待樣式表、圖像和子框架完成加載;
  • readystatechange: 一個document 的 Document.readyState 屬性描述了文檔的加載狀態(tài),當這個狀態(tài)發(fā)生了變化,就會觸發(fā)該事件;
  • load: 當一個資源及其依賴資源已完成加載時,將觸發(fā)load事件;
  • beforeunload: 當瀏覽器窗口,文檔或其資源將要卸載時,會觸發(fā)beforeunload事件。
  • unload: 當文檔或一個子資源正在被卸載時, 觸發(fā) unload事件。

細心點會發(fā)現(xiàn)上面在介紹事件時提到了 UIEvent 以及 Event,這是什么呢?這些都是事件——可以被 JavaScript 偵測到的行為。其他的事件接口還包括 KeyboardEvent / VRDisplayEvent (是的,沒錯,這就是你感興趣且熟知的那個 VR)等等;如果在搜索引擎中稍加搜索,你會發(fā)現(xiàn)有些資料里寫到事件可以分為以下幾類:

  • UI事件
  • 焦點事件
  • 鼠標與滾輪事件
  • 鍵盤與文本事件
  • 復合事件
  • 變動事件
  • HTML5 事件
  • 設備事件
  • 觸摸與手勢事件

但這樣寫實在有些凌亂,其中一些是 DOM3 定義的事件,有一些是單獨列出的事件,如果你覺得熟悉那么你會發(fā)現(xiàn)這是 JavaScript 高級程序設計里的敘述模式,在我看來,理解這些事件可以按照 DOM3 事件以及其他事件來做區(qū)分:其中,DOM3 級事件規(guī)定了以下幾類事件 – UI 事件, 焦點事件, 鼠標事件, 滾輪事件, 文本事件, 鍵盤事件, 合成事件, 變動事件, 變動名稱事件; 而剩下的例如 HTML5 事件可以單獨做了解。而剛開始提到的 Event 作為一個主要接口,是很多事件的實現(xiàn)父類。有關 Web API 接口可以在這里查到,里面可以看到有很多 Event 字眼。

好吧,事件說了這么多,我們還是沒有解決剛開始提出的問題,如果監(jiān)聽頁面中動態(tài)生成的元素呢?想到動態(tài)生成的元素都是需要通過網(wǎng)絡請求獲取資源的,那么是否可以監(jiān)聽所有 HTTP 請求呢?查看 jQuery 文檔可以知道每當一個Ajax請求完成,jQuery 就會觸發(fā) ajaxComplete 事件,在這個時間點所有處理函數(shù)會使用 .ajaxComplete() 方法注冊并執(zhí)行。但是誰能保證所有 ajax 都從 jQuery 走呢?所以應該在變動事件中做出選擇,我們來看看 DOM2 定義的如下變動事件:

  • DOMSubtreeModified: 在DOM結構發(fā)生任何變化的時候。這個事件在其他事件觸發(fā)后都會觸發(fā);
  • DOMNodeInserted: 當一個節(jié)點作為子節(jié)點被插入到另一個節(jié)點中時觸發(fā);
  • DOMNodeRemoved: 在節(jié)點從其父節(jié)點中移除時觸發(fā);
  • DOMNodeInsertedIntoDocument: 在一個節(jié)點被直接插入文檔或通過子樹間接插入文檔之后觸發(fā)。這個事件在 DOMNodeInserted 之后觸發(fā);
  • DOMNodeRemovedFromDocument: 在一個節(jié)點被直接從文檔移除或通過子樹間接從文檔移除之前觸發(fā)。這個事件在 DOMNodeRemoved 之后觸發(fā);
  • DOMAttrModified: 在特性被修改之后觸發(fā);
  • DOMCharacterDataModified: 在文本節(jié)點的值發(fā)生變化時觸發(fā);

所以,用 DOMSubtreeModified 好像沒錯。師兄旁邊提醒,用 MutationObserver, 于是又搜到了一個新大陸。MDN 這樣描述 MutationObserver:

MutationObserver給開發(fā)者們提供了一種能在某個范圍內(nèi)的DOM樹發(fā)生變化時作出適當反應的能力.該API設計用來替換掉在DOM3事件規(guī)范中引入的Mutation事件.

DOM3 事件規(guī)范中的 Mutation 事件可以被簡單看成是 DOM2 事件規(guī)范中定義的 Mutation 事件的一個擴展,但是這些都不重要了,因為他們都要被 MutationObserver 替代了。好了,那么來詳細介紹一下 MutationObserver 吧。文章《Mutation Observer API》對 MutationObserver 的用法介紹的比較詳細,所以我挑幾點能直接解決我們需求的說一說。

既然要監(jiān)聽 DOM 的變化,我們來看看 Observer 的作用都有哪些:

它等待所有腳本任務完成后,才會運行,即采用異步方式。

它把 DOM 變動記錄封裝成一個數(shù)組進行處理,而不是一條條地個別處理 DOM 變動。

它既可以觀察發(fā)生在 DOM 的所有類型變動,也可以觀察某一類變動。

MutationObserver 的構造函數(shù)比較簡單,傳入一個回調函數(shù)即可(回調函數(shù)接受兩個參數(shù),***個是變動數(shù)組,第二個是觀察器實例):

  1. let observer = new MutationObserver(callback); 

觀察器實例使用 observe 方法來監(jiān)聽, disconnect 方法停止監(jiān)聽,takeRecords 方法來清除變動記錄。

  1. let article = document.body; 
  2.  
  3.   
  4.  
  5. let  options = { 
  6.  
  7.   'childList'true
  8.  
  9.   'attributes':true 
  10.  
  11. } ; 
  12.  
  13.   
  14.  
  15. observer.observe(article, options);  

observe 方法中***個參數(shù)是所要觀察的變動 DOM 元素,第二個參數(shù)則接收所要觀察的變動類型(子節(jié)點變動和屬性變動)。變動類型包括以下幾種:

  • childList:子節(jié)點的變動。
  • attributes:屬性的變動。
  • characterData:節(jié)點內(nèi)容或節(jié)點文本的變動。
  • subtree:所有后代節(jié)點的變動。

想要觀察哪一種變動類型,就在 option 對象中指定它的值為 true。需要注意的是,如果設置觀察 subtree 的變動,必須同時指定 childList、attributes 和 characterData 中的一種或多種。disconnect 方法和 takeRecords 方法則直接調用即可,無傳入?yún)?shù)。

好的,我們已經(jīng)搞定了 DOM 變動的監(jiān)聽,將代碼刷新一下看下效果吧,因為頁面由很多動態(tài)生成的商品組成,那么我應該在 body 上添加變動監(jiān)聽,所以 options 應該這樣設置:

  1. var options = { 
  2.  
  3.   'attributes'true
  4.  
  5.   'subtree'true 
  6.  
  7.  

咦?頁面往下拉一小點就觸發(fā)了 observer 幾十次?這樣 DOM 哪吃得消啊,查看了頁面的變動記錄發(fā)現(xiàn)每次新進的資源底層都調用了 Node.insertBefore() 方法…

再聊聊 JavaScript 中的截流/節(jié)流函數(shù)

現(xiàn)在遇到的一個麻煩是, DOM 變動太頻繁了,如果每次變動都監(jiān)聽那真是太耗費資源了。一個簡單的解決辦法是我就放棄監(jiān)聽了,而采用 setInterval 方法定時執(zhí)行更新邏輯。是的,雖然方法原始了一點,但是性能上比 Observer “改進”了不少。

這個時候,又來了師兄的助攻:“用用截流函數(shù)”。記起之前看《JavaScript 語言精粹》的時候看到是用 setTimeout 方法自調用來解決 setInteval 的頻繁執(zhí)行吃資源的現(xiàn)象,不知道兩者是不是有關聯(lián)。網(wǎng)上一查發(fā)現(xiàn)有兩個“jie流函數(shù)”。需求來自于這里:

在前端開發(fā)中,頁面有時會綁定scroll或resize事件等頻繁觸發(fā)的事件,也就意味著在正常的操作之內(nèi),會多次調用綁定的程序,然而有些時候javascript需要處理的事情特別多,頻繁出發(fā)就會導致性能下降、成頁面卡頓甚至是瀏覽器奔潰。

如果重復利用 setTimeout 和 clearTimeout 方法,我們好像可以解決這個頻繁觸發(fā)的執(zhí)行。每次事件觸發(fā)的時候我首先判斷一下當前有沒有一個 setTimeout 定時器,如果有的話我們先將它清除,然后再新建一個 setTimeout 定時器來延遲我的響應行為。這樣聽上去還不錯,因為我們每次都不立即執(zhí)行我們的響應,而頻繁觸發(fā)過程我們又能保持響應函數(shù)一直存在(且只存在一個),除了會有些延遲響應外,沒什么不好的。是的這就是截流函數(shù)(debounce),有一篇博客用這個小故事介紹它:

形像的比喻是橡皮球。如果手指按住橡皮球不放,它就一直受力,不能反彈起來,直到松手。debounce 的關注點是空閑的間隔時間。

在我的業(yè)務中,在 observer 實例中調用下面寫的這個截流函數(shù)就可以啦

  1. /** 
  2.  
  3. * fn 執(zhí)行函數(shù) 
  4.  
  5. * context 綁定上下文 
  6.  
  7. * timeout 延時數(shù)值 
  8.  
  9. **/ 
  10.  
  11. let debounce = function(fn, context, timeout) { 
  12.  
  13. let timer; 
  14.  
  15.      
  16.  
  17.     // 利用閉包將內(nèi)容傳遞出去 
  18.  
  19. return function() { 
  20.  
  21.   if (timer) { 
  22.  
  23.     // 清除定時器 
  24.  
  25.     clearTimeout(timer); 
  26.  
  27.   } 
  28.  
  29.   // 設置一個新的定時器 
  30.  
  31.   timer = setTimeout(function(){ 
  32.  
  33.   fn.apply(context, arguments) 
  34.  
  35.   }, timeout); 
  36.  
  37.  } 
  38.  
  39.  

當然,解決了自己的問題,但還有一個概念沒有說到——“節(jié)流函數(shù)”。同一篇博文里也使用了一個例子來說明它:

形像的比喻是水龍頭或機槍,你可以控制它的流量或頻率。throttle 的關注點是連續(xù)的執(zhí)行間隔時間。

函數(shù)節(jié)流的原理也挺簡單,一樣還是定時器。當我觸發(fā)一個時間時,先setTimout讓這個事件延遲一會再執(zhí)行,如果在這個時間間隔內(nèi)又觸發(fā)了事件,那我們就清除原來的定時器,再setTimeout一個新的定時器延遲一會執(zhí)行。函數(shù)節(jié)流的出發(fā)點,就是讓一個函數(shù)不要執(zhí)行得太頻繁,減少一些過快的調用來節(jié)流。這里用 AlloyTeam 的節(jié)流代碼實現(xiàn)來解釋:

  1. // 參數(shù)同上 
  2.  
  3. var throttle = function(fn, delay, mustRunDelay){ 
  4.  
  5.  var timer = null
  6.  
  7.  var t_start; 
  8.  
  9.  return function(){ 
  10.  
  11.     var context = this, args = arguments, t_curr = +new Date(); 
  12.  
  13.      
  14.  
  15.     // 清除定時器 
  16.  
  17.     clearTimeout(timer); 
  18.  
  19.      
  20.  
  21.     // 函數(shù)初始化判斷 
  22.  
  23.     if(!t_start){ 
  24.  
  25.         t_start = t_curr; 
  26.  
  27.     } 
  28.  
  29.      
  30.  
  31.     // 超時(指定的時間間隔)判斷 
  32.  
  33.     if(t_curr - t_start >= mustRunDelay){ 
  34.  
  35.         fn.apply(context, args); 
  36.  
  37.         t_start = t_curr; 
  38.  
  39.     } 
  40.  
  41.     else { 
  42.  
  43.         timer = setTimeout(function(){ 
  44.  
  45.             fn.apply(context, args); 
  46.  
  47.         }, delay); 
  48.  
  49.     } 
  50.  
  51.  }; 
  52.  
  53. };  

當然,AlloyTeam 那篇文章將這里所說的截流函數(shù)作為節(jié)流函數(shù)的 V1.0 版本,你也可以這樣認為。畢竟,設置了必然觸發(fā)執(zhí)行的時間間隔(即 mustRunDelay 函數(shù)),可以使得截流函數(shù)不會在“瘋狂事件”情況下無止境的循環(huán)下去。

Observer 和截流函數(shù)一結合,問題解決啦嘿嘿。當然還有很多坑,下次再開一篇說說吧。

參考

  • https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded
  • https://developer.mozilla.org/zh-CN/docs/Web/Events/load
  • https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
  • http://www.cnblogs.com/fsjohnhuang/p/4147810.html
  • http://www.alloyteam.com/2012/11/javascript-throttle/
責任編輯:龐桂玉 來源: 前端大全
相關推薦

2022-05-16 08:09:45

前端API

2021-01-11 07:51:16

DOM對象節(jié)點樹

2013-02-21 09:54:12

響應式重構Web

2017-10-17 15:40:25

javascript刷新頁面

2021-01-18 07:15:22

虛擬DOM真實DOMJavaScript

2023-11-21 16:02:56

2024-01-15 09:23:16

框架方式原生

2010-09-28 14:44:56

遍歷DOM

2021-09-03 13:35:54

服務器服務器蔓延管理服務器

2018-10-24 15:33:49

單屏頁面適配玩

2023-08-31 08:28:13

Java應用

2012-12-27 09:49:21

Web響應式

2011-10-21 09:06:41

Better Net銳捷網(wǎng)絡

2020-10-22 10:17:27

敏捷性高級合伙人CIO

2023-08-08 13:51:13

Gherkin開發(fā)

2021-07-14 09:45:24

設計師約束布局界面布局

2021-04-09 18:01:03

前端ReactDOM

2022-01-12 09:43:20

惡意軟件iPhone攻擊

2010-09-08 16:50:11

JavaScriptDOM操作

2013-07-10 13:13:25

頁面設計響應式
點贊
收藏

51CTO技術棧公眾號