溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

javascript中如何監(jiān)聽頁(yè)面DOM變動(dòng)并高效響應(yīng)

發(fā)布時(shí)間:2021-11-15 15:22:25 來(lái)源:億速云 閱讀:120 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“javascript中如何監(jiān)聽頁(yè)面DOM變動(dòng)并高效響應(yīng)”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

從 DOM 變動(dòng)事件監(jiān)聽說(shuō)起

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

javascript中如何監(jiān)聽頁(yè)面DOM變動(dòng)并高效響應(yīng)

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

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

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

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

<script>    window.addEventListener("load", function(event) {      console.log("All resources finished loading!");    });        // or    window.onload=function(){      console.log("All resources finished loading!");    };        // HTML  < body onload="SomeJavaScriptCode">        // jQuery    $( window ).on( "load", handler )  </script>

當(dāng)然,說(shuō)到 onload 事件,有一個(gè) jQuery 中相似的事件一定會(huì)被提及&mdash;&mdash; ready 事件。jQuery 中這樣定義這個(gè)事件:

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

需要知道的是 jQuery 定義的 ready 事件實(shí)質(zhì)上是為 DOMContentLoaded 事件設(shè)計(jì)的,所以當(dāng)我們談?wù)摷虞d時(shí)應(yīng)該區(qū)分的事件其實(shí)是  onload(接口 UIEvent) 以及 DOMContentLoaded(接口 Event),MDN 這樣描述 DOMContentLoaded:

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

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

  • DOMContentLoaded: 當(dāng)初始HTML文檔被完全加載和解析時(shí),DOMContentLoaded  事件被觸發(fā),而無(wú)需等待樣式表、圖像和子框架完成加載;

  • readystatechange: 一個(gè)document 的 Document.readyState  屬性描述了文檔的加載狀態(tài),當(dāng)這個(gè)狀態(tài)發(fā)生了變化,就會(huì)觸發(fā)該事件;

  • load: 當(dāng)一個(gè)資源及其依賴資源已完成加載時(shí),將觸發(fā)load事件;

  • beforeunload: 當(dāng)瀏覽器窗口,文檔或其資源將要卸載時(shí),會(huì)觸發(fā)beforeunload事件。

  • unload: 當(dāng)文檔或一個(gè)子資源正在被卸載時(shí), 觸發(fā) unload事件。

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

  • UI事件

  • 焦點(diǎn)事件

  • 鼠標(biāo)與滾輪事件

  • 鍵盤與文本事件

  • 復(fù)合事件

  • 變動(dòng)事件

  • HTML5 事件

  • 設(shè)備事件

  • 觸摸與手勢(shì)事件

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

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

  • DOMSubtreeModified: 在DOM結(jié)構(gòu)發(fā)生任何變化的時(shí)候。這個(gè)事件在其他事件觸發(fā)后都會(huì)觸發(fā);

  • DOMNodeInserted: 當(dāng)一個(gè)節(jié)點(diǎn)作為子節(jié)點(diǎn)被插入到另一個(gè)節(jié)點(diǎn)中時(shí)觸發(fā);

  • DOMNodeRemoved: 在節(jié)點(diǎn)從其父節(jié)點(diǎn)中移除時(shí)觸發(fā);

  • DOMNodeInsertedIntoDocument: 在一個(gè)節(jié)點(diǎn)被直接插入文檔或通過(guò)子樹間接插入文檔之后觸發(fā)。這個(gè)事件在  DOMNodeInserted 之后觸發(fā);

  • DOMNodeRemovedFromDocument: 在一個(gè)節(jié)點(diǎn)被直接從文檔移除或通過(guò)子樹間接從文檔移除之前觸發(fā)。這個(gè)事件在  DOMNodeRemoved 之后觸發(fā);

  • DOMAttrModified: 在特性被修改之后觸發(fā);

  • DOMCharacterDataModified: 在文本節(jié)點(diǎn)的值發(fā)生變化時(shí)觸發(fā);

所以,用 DOMSubtreeModified 好像沒(méi)錯(cuò)。師兄旁邊提醒,用 MutationObserver, 于是又搜到了一個(gè)新大陸。MDN 這樣描述  MutationObserver:

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

DOM3 事件規(guī)范中的 Mutation 事件可以被簡(jiǎn)單看成是 DOM2 事件規(guī)范中定義的 Mutation  事件的一個(gè)擴(kuò)展,但是這些都不重要了,因?yàn)樗麄兌家?MutationObserver 替代了。好了,那么來(lái)詳細(xì)介紹一下 MutationObserver  吧。文章《Mutation Observer API》對(duì) MutationObserver  的用法介紹的比較詳細(xì),所以我挑幾點(diǎn)能直接解決我們需求的說(shuō)一說(shuō)。

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

它等待所有腳本任務(wù)完成后,才會(huì)運(yùn)行,即采用異步方式。

它把 DOM 變動(dòng)記錄封裝成一個(gè)數(shù)組進(jìn)行處理,而不是一條條地個(gè)別處理 DOM 變動(dòng)。

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

MutationObserver 的構(gòu)造函數(shù)比較簡(jiǎn)單,傳入一個(gè)回調(diào)函數(shù)即可(回調(diào)函數(shù)接受兩個(gè)參數(shù),***個(gè)是變動(dòng)數(shù)組,第二個(gè)是觀察器實(shí)例):

let observer = new MutationObserver(callback);

觀察器實(shí)例使用 observe 方法來(lái)監(jiān)聽, disconnect 方法停止監(jiān)聽,takeRecords 方法來(lái)清除變動(dòng)記錄。

let article = document.body;     let  options = {    'childList': true,    'attributes':true  } ;     observer.observe(article, options);

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

  • childList:子節(jié)點(diǎn)的變動(dòng)。

  • attributes:屬性的變動(dòng)。

  • characterData:節(jié)點(diǎn)內(nèi)容或節(jié)點(diǎn)文本的變動(dòng)。

  • subtree:所有后代節(jié)點(diǎn)的變動(dòng)。

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

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

var options = {    'attributes': true,    'subtree': true  }

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

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

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

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

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

如果重復(fù)利用 setTimeout 和 clearTimeout 方法,我們好像可以解決這個(gè)頻繁觸發(fā)的執(zhí)行。每次事件觸發(fā)的時(shí)候我首先判斷一下當(dāng)前有沒(méi)有一個(gè)  setTimeout 定時(shí)器,如果有的話我們先將它清除,然后再新建一個(gè) setTimeout  定時(shí)器來(lái)延遲我的響應(yīng)行為。這樣聽上去還不錯(cuò),因?yàn)槲覀兠看味疾涣⒓磮?zhí)行我們的響應(yīng),而頻繁觸發(fā)過(guò)程我們又能保持響應(yīng)函數(shù)一直存在(且只存在一個(gè)),除了會(huì)有些延遲響應(yīng)外,沒(méi)什么不好的。是的這就是截流函數(shù)(debounce),有一篇博客用這個(gè)小故事介紹它:

形像的比喻是橡皮球。如果手指按住橡皮球不放,它就一直受力,不能反彈起來(lái),直到松手。debounce 的關(guān)注點(diǎn)是空閑的間隔時(shí)間。

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

/**  * fn 執(zhí)行函數(shù)  * context 綁定上下文  * timeout 延時(shí)數(shù)值  **/  let debounce = function(fn, context, timeout) {  let timer;            // 利用閉包將內(nèi)容傳遞出去  return function() {    if (timer) {      // 清除定時(shí)器      clearTimeout(timer);    }    // 設(shè)置一個(gè)新的定時(shí)器    timer = setTimeout(function(){    fn.apply(context, arguments)    }, timeout);   }  }

當(dāng)然,解決了自己的問(wèn)題,但還有一個(gè)概念沒(méi)有說(shuō)到&mdash;&mdash;“節(jié)流函數(shù)”。同一篇博文里也使用了一個(gè)例子來(lái)說(shuō)明它:

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

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

// 參數(shù)同上  var throttle = function(fn, delay, mustRunDelay){   var timer = null;   var t_start;   return function(){      var context = this, args = arguments, t_curr = +new Date();            // 清除定時(shí)器      clearTimeout(timer);            // 函數(shù)初始化判斷      if(!t_start){          t_start = t_curr;      }            // 超時(shí)(指定的時(shí)間間隔)判斷      if(t_curr - t_start >= mustRunDelay){          fn.apply(context, args);          t_start = t_curr;      }      else {          timer = setTimeout(function(){              fn.apply(context, args);          }, delay);      }   };  };

“javascript中如何監(jiān)聽頁(yè)面DOM變動(dòng)并高效響應(yīng)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI