Dom-api MutationObserver使用方法詳解
1. 概述
MutationObserver 接口提供了監(jiān)視對 DOM 樹所做更改的能力。它被設(shè)計為舊的 Mutation Events 功能的替代品,該功能是 DOM3 Events 規(guī)范的一部分。 - MDN
也就是說,當(dāng)監(jiān)視的 DOM
發(fā)生變動時 MutationObserver
將收到通知并觸發(fā)事先設(shè)定好的回調(diào)函數(shù)。這個功能非常強大,意味著對于我們可以更加方便的動態(tài)操作 DOM
元素了。
你是否能聯(lián)想到某些業(yè)務(wù)場景呢?
像這樣的列表頁,由于文案和文章配圖數(shù)量的不同導(dǎo)致有多種不同的 ui 設(shè)計和排列方式,所以在前端對數(shù)據(jù)渲染的時候,要對列表每一項內(nèi)容類型進行甄別。使用 MutationObserver
可以非常簡單的完成這個需求
2. 基本使用
MutationObserver
是一個構(gòu)造函數(shù),通過調(diào)用 MutationObserver 構(gòu)造函數(shù)并傳入一個回調(diào)函數(shù)來創(chuàng)建一個觀察 DOM
的實例
const observer = new MutationObserver(() => console.log('DOM 發(fā)生變化了~'));
回調(diào)參數(shù)的兩個參數(shù):
mutationRecords
:數(shù)組隊列,記錄操作的結(jié)果observer
:與構(gòu)造函數(shù)的返回值 全等,因為這個回調(diào)函數(shù)是 異步執(zhí)行,所以也可以訪問到外部的observer
后文還會再詳細討論這兩個參數(shù)
2.1 observer 方法
新創(chuàng)建的 MutationObserver
實例不會關(guān)聯(lián) DOM 的任何部分。要把這個 observer
與 DOM
關(guān)聯(lián)起來,需要使用 observe()
方法
observer.observe(document.body, { attributes: true });
這個方法接收必需的參數(shù):
- 第一個參數(shù):要觀察的
DOM
節(jié)點 - 第二個參數(shù):
MutationObserverInit
對象
這樣 document.body
就被觀察了,只要 document.body
元素的任何屬性值發(fā)生變化,就會觸發(fā)觀察對象,并且 異步調(diào)用 傳入 MutationObserver
的回調(diào)函數(shù)(這是一個 微任務(wù))
const observer = new MutationObserver(() => console.log('DOM 發(fā)生變化了~')); observer.observe(document.body, { attributes: true }); setTimeout(() => { document.body.className = 'test'; }, 1000);
等過了一秒之后,定時器的回調(diào)函數(shù)執(zhí)行,修改了 document.body
的 class
屬性,所以了觸發(fā) MutationObserver
的回調(diào)函數(shù)
2.2 MutationObserverInit 對象
在上面的例子中,只要 document.body
本身的任意屬性發(fā)生了,都會被觀察到,但是其他修改 DOM
的行為不會被觀察,例如節(jié)點的增刪改查,子節(jié)點屬性的修改...,因為我們在調(diào)用 observe()
方法的時候傳入的 MutationObserverInit
對象添加了 attributes
屬性,所以 observe()
方法作用是只能偵測自身的元素屬性值的變化。MutationObserverInit
對象除了這個屬性之外,還有很多非常強大的屬性可以觀察更多的節(jié)點操作
MutationObserverInit
對象用于控制對目標節(jié)點的觀察范圍。觀察方式的類型有 屬性變化、文本變化 和 子節(jié)點變化 這三種。
所以在調(diào)用 observe()
時,MutationObserverInit
對象中的 attribute
(屬性變化)、characterData
(文本變化) 和 childList
(子節(jié)點變化) 屬性必須 至少有一項 為 true
(無論是直接設(shè)置這幾個屬性,還是通過設(shè)置 attributeOldValue
(屬性變化)等屬性間接導(dǎo)致它們的值轉(zhuǎn)換為 true
)。否則會拋出錯誤,因為 DOM
的變化不會被任何變化事件類型觸發(fā)回調(diào)。
- 屬性變化
觀察節(jié)點 屬性 的 添加、移除 和 修改。需要在 MutationObserverInit
對象中將 attributes
屬性設(shè)置為 true
const observer = new MutationObserver(() => console.log('DOM 發(fā)生變化了~')); observer.observe(document.body, { attributes: true }); setTimeout(() => { document.body.className = 'test'; }, 1000);
還有 attributeOldValue: true
:可以記錄變化之前的屬性值。attributeFilter: ['class', 'id']
:可以觀察哪些屬性的變化,在這里只觀察了 class
和 id
屬性
- 文本變化
觀察文本節(jié)點(如 Text 文本節(jié)點、Comment 注釋 ) 中字符的 添加、刪除 和 修改。要在 MutationObserverInit
對象中將 characterData
屬性設(shè)置為 true
const observer = new MutationObserver(() => console.log('DOM 發(fā)生變化了~')); observer.observe(document.body.firstChild, { characterData: true }); setTimeout(() => { document.body.firstChild.textContent = '123'; }, 1000);
還有 characterDataOldValue
:可以記錄變化之前的文本值
- 觀察子節(jié)點
觀察目標節(jié)點子節(jié)點的添加和移除。需要在 MutationObserverInit
對象中將 childList
屬性設(shè)置為 true
const observer = new MutationObserver(() => console.log('DOM 發(fā)生變化了~')); observer.observe(document.body, { childList: true }); setTimeout(() => { document.body.appendChild(document.createElement('div')); }, 1000);
在這個例子中控制臺輸出兩次,第一次是 body
元素在 0s
觸發(fā)回調(diào),第二次才是新創(chuàng)建的元素在 1s
之后觸發(fā)回調(diào),因為觀察 document.body
會在創(chuàng)建 body
的時候就立即被觀察到,而觀察非 body
元素,不會觸發(fā)自身創(chuàng)建的過程
childList
只會觀察子節(jié)點,但不會觀察深層的節(jié)點,可以在 MutationObserverInit
對象中將 subtree
屬性設(shè)置為 true
,還得將 childList
為 true
,因為 MutationObserverInit
對象中的 attribute
、characterData
和 childList
屬性必須 至少有一項 為 true
<div></div> <script> const observer = new MutationObserver(mutationRecords => { console.log('觸發(fā)了'); console.log(mutationRecords.length); // 2 }); observer.observe(document.body.children[0], { childList: true, subtree: true }); setTimeout(() => { document.body.children[0].appendChild(document.createElement('div')); document.body.children[0].children[0].appendChild(document.createElement('div')); }, 1000); </script>
這里雖然只會觸發(fā)一次回調(diào),但是會在 mutationRecords
這個數(shù)組中會分別記下兩次 DOM
操作的記錄,所以數(shù)組的長度為 2
<div></div> <script> const observer = new MutationObserver(mutationRecords => { console.log('觸發(fā)了'); console.log(mutationRecords.length); // 1 }); observer.observe(document.body.children[0], { childList: true, subtree: true }); setTimeout(() => { document.body.children[0].appendChild(document.createElement('div')); }, 1000); setTimeout(() => { document.body.children[0].children[0].appendChild(document.createElement('div')); }, 1000); </script>
這個例子與上個例子區(qū)別是將兩次 DOM
操作放在兩個不同的定時器執(zhí)行,但是結(jié)果卻是截然不同,這里會輸出兩次,mutationRecords
數(shù)組的長度為 1
這是因為 DOM
操作是同步的,DOM
渲染是異步的,MutationObserver
中的回調(diào)函數(shù)執(zhí)行會被包裹在一個 微任務(wù) 中,而定時器是 宏任務(wù),所以整個執(zhí)行過程是:第一個定時器先執(zhí)行,觀察 DOM
的回調(diào)函數(shù)執(zhí)行,第二個定時器再執(zhí)行,所以 DOM
變化被觀察了兩次。
上一個的例子 DOM
操作是在同一個 宏任務(wù)
中執(zhí)行,因為瀏覽器會優(yōu)化 DOM
渲染的過程,所以等到兩個 div
元素創(chuàng)建完畢才會渲染,之后執(zhí)行觀察 DOM
的 微任務(wù),所以才會觸發(fā)一次觀察,但是產(chǎn)生了兩個結(jié)果,所以 mutationRecords
數(shù)組的長度為 2
這里還有一個怪異現(xiàn)象,在第二個例子中,為什么兩輸出 mutationRecords
的長度都是 1
,因為這兩個數(shù)組不是同一個數(shù)組,關(guān)于為什么 mutationRecords
數(shù)組 不會緩存 第一次的操作結(jié)果,而是創(chuàng)建兩個不同的數(shù)組,會在后面的內(nèi)容詳細討論。
2.3 disconnect()方法
默認情況下,只要被觀察的元素不被垃圾回收,MutationObserver
的回調(diào)就會響應(yīng) DOM
變化事件,從而被執(zhí)行。想要 提前終止執(zhí)行 回調(diào),可以調(diào)用 disconnect()
方法。
<div></div> <script> const observer = new MutationObserver(mutationRecords => { console.log('觸發(fā)了'); }); observer.observe(document.body.children[0], { childList: true, subtree: true }); setTimeout(() => { document.body.children[0].appendChild(document.createElement('div')); setTimeout(() => { observer.disconnect(); }, 0); }, 1000); setTimeout(() => { document.body.children[0].appendChild(document.createElement('div')); }, 2000); </script>
在這個例子中,在第一秒的時候執(zhí)行了 DOM
操作,并且創(chuàng)建一個定時器包裹 disconnect()
方法,然后執(zhí)行 disconnect()
方法,在第二秒的時候執(zhí)行了另外一個 DOM
操作。所以結(jié)果只有第一次 DOM
操作會被觀察到
為什么這里需要將 disconnect
方法計時器里執(zhí)行呢,千萬別忘了,DOM
操作是 同步執(zhí)行 的,DOM
渲染是 異步執(zhí)行 的,disconnect()
也是 同步執(zhí)行 的。如果不添加定時器,在 DOM
渲染值之前就取消了觀察,雖然操作了 DOM
,但是渲染過程并沒有觀察到
2.4 takeRecords
調(diào)用 MutationObserver
實例的 takeRecords()
方法可以清空記錄隊列,取出并返回其中的所有 MutationRecord
實例。
const observer = new MutationObserver(mutationRecords => { console.log(mutationRecords); // 不輸出 }); observer.observe(document.body, { attributes: true }); document.body.className = 'test1'; document.body.className = 'test2'; document.body.className = 'test3'; console.log(observer.takeRecords().length); // 3 console.log(observer.takeRecords().length); // 0
在這個例子中,操作了 3
次 DOM
,所以在調(diào)用第一次 takeRecords()
方法的時候會輸出 3
,并且切斷了與觀察對象的聯(lián)系,所以不會觸發(fā) MutationObserver
的回調(diào),但是這種切斷關(guān)系是 不牢靠 的,也就意味著下次的 DOM
操作會 重啟觀察,就像下面的這個例子表現(xiàn)的一樣
const observer = new MutationObserver(mutationRecords => { console.log(mutationRecords); // 輸出兩次 }); observer.observe(document.body.children[0], { attributes: true }); document.body.children[0].className = 'test1'; document.body.children[0].className = 'test2'; document.body.children[0].className = 'test3'; observer.takeRecords(); document.body.children[0].className = 'test4'; setTimeout(() => { document.body.children[0].className = 'test5'; });
3. MutationRecord
MutationRecord
是一個 記錄隊列 的數(shù)組,,僅當(dāng) 微任務(wù)隊列 沒有其他的微任務(wù)回調(diào)時(隊列中微任務(wù) 長度為 0
),才會將觀察者注冊的 回調(diào) 作為微任務(wù)放置到任務(wù)隊列上。這樣可以保證記錄隊列的內(nèi)容不會被回調(diào)處理兩次。
在回調(diào)的微任務(wù)異步執(zhí)行期間,有可能又會發(fā)生更多變化事件。因此被調(diào)用的回調(diào)會接收到一個 MutationRecord
實例的數(shù)組,順序為它們進入記錄隊列的順序。回調(diào)要負責(zé)處理這個數(shù)組的每一個實例,因為 回調(diào)函數(shù) 退出之后這些實現(xiàn)就不存在了?;卣{(diào)函數(shù)執(zhí)行完成后,這些 MutationRecord
就用不著了, 因此記錄隊列會被清空,其內(nèi)容會被丟棄。所以每一個回調(diào)函數(shù)中的 MutationRecords
數(shù)組是 不同的實例
3.1 MutationRecord 實例
const observer = new MutationObserver(mutationRecords => { console.log(mutationRecords); }); const oDiv = document.getElementsByTagName('div')[0]; observer.observe(oDiv, { attributeOldValue: true }); oDiv.classList.add('box');
幾個重要的屬性:
屬性 | 說明 |
---|---|
target | 被修改影響的目標節(jié)點 |
type | 表示變化的類型:"attributes"、"characterData"或"childList" |
oldValue | 如果在 MutationObserverInit 對象中啟用(attributeOldValue 或 characterData OldValue 為 true),"attributes"或"characterData"的變化事件會設(shè)置這個屬性為被替代的值 "childList"類型的變化始終將這個屬性設(shè)置為 null |
addedNodes | 對于"childList"類型的變化,返回包含變化中添加節(jié)點的 NodeList 默認為空 NodeList |
4. MutationObserver 實戰(zhàn)
一個簡單的業(yè)務(wù)場景:
用戶提交評論,如果評論的內(nèi)容超過最大寬度,需要隱藏多余的部分,同時展示“查看更多”按鈕,點擊這個按鈕就會展示評論的全部內(nèi)容
難點:只有當(dāng) DOM
被渲染的時候 才知道實際的高度,所以無法預(yù)先分析評論文本內(nèi)容而選擇渲染方式的類型
實現(xiàn)思路:使用 MutationObserver
監(jiān)聽評論區(qū)列表,每當(dāng)用戶提交新的評論,新生成的 DOM
就會被觀察到,判斷評論的內(nèi)容是否超出最大高度,更新 UI
<script lang="ts" setup> import { onMounted, reactive, ref } from 'vue'; interface ICommentItem { id: string; text: string; showBtn: boolean; } const comIptVal = ref(''); const commentList = reactive<ICommentItem[]>([]); const commentListRef = ref<HTMLElement | null>(null); const MaxSize = 50; // 每一項最大高度 const observer = new MutationObserver(mutationRecord => { const currRecord = mutationRecord[mutationRecord.length - 1]; // 最新的記錄 const newNode = currRecord.addedNodes[currRecord.addedNodes.length - 1] as HTMLElement; // 新添加的節(jié)點 // 新增加的按鈕也會觸發(fā)觀察,所以要判斷新增加節(jié)點是否是評論 if (newNode.className === 'comment-item') { const id = newNode.dataset.id; const item = commentList.find(item => item.id === id)!; if (newNode.clientHeight > MaxSize) { // 如果超出最大高度 const oText = newNode.children[0] as HTMLElement; oText.style.height = MaxSize + 'px'; oText.style.overflow = 'hidden'; item.showBtn = true; } } }); onMounted(() => { observer.observe(commentListRef.value as HTMLElement, { subtree: true, childList: true, }); }); const addCommentItem = () => { commentList.push({ id: String(new Date().getTime()), // 評論的 id text: parseComment(comIptVal.value), // 解析輸入文本內(nèi)容 showBtn: false, // 默認不超出最大高度 }); }; const parseComment = (str: string) => { return str.replace(/[\n\r]/g, '<br />'); // 將 \n 換行解析成 <br /> 元素 }; const showAllBtnClick = (el: HTMLElement, item: ICommentItem) => { el.style.overflow = 'visible'; el.style.height = 'auto'; item.showBtn = false; // 隱藏點擊更多按鈕 }; const child = reactive<HTMLElement[]>([]); // 循環(huán)綁定 DOM </script> <template> <textarea v-model="comIptVal"></textarea> <button @click="addCommentItem">添加</button> <ul class="comment-list" ref="commentListRef"> <li class="comment-item" v-for="(item, index) in commentList" :key="item.id" :data-id="item.id"> <div v-html="item.text" :ref="(el: any) => child[index] = el"></div> <button v-if="item.showBtn" @click="showAllBtnClick(child[index] as HTMLElement, item)"> 更多 </button> </li> </ul> </template>
參考文獻
JavaScript 高級程序設(shè)計第 4 版.PDF – 1024.Cool
總結(jié)
MutationObserver
api 使用大多數(shù)場景為:動態(tài)監(jiān)聽 DOM
元素的變化,在傳入構(gòu)造函數(shù)的回調(diào)函數(shù)中可以訪問到觸發(fā) DOM
變化的 target
和 影響 DOM
變化的結(jié)果
以上就是Dom-api MutationObserver使用方法詳解的詳細內(nèi)容,更多關(guān)于Dom-api MutationObserver方法的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS版的date函數(shù)(和PHP的date函數(shù)一樣)
這篇文章主要介紹了JS版的date函數(shù),使用方法和PHP的date函數(shù)一樣,需要的朋友可以參考下2014-05-05微信小程序 定位到當(dāng)前城市實現(xiàn)實例代碼
這篇文章主要介紹了微信小程序 定位到當(dāng)前城市實現(xiàn)實例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02自行實現(xiàn)Promise.allSettled的Polyfill處理
這篇文章主要為大家介紹了自行實現(xiàn)Promise.allSettled?的?Polyfill處理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08JavaScript 實現(xiàn)點擊關(guān)閉全屏示例詳解
這篇文章主要為大家介紹了JavaScript 實現(xiàn)點擊關(guān)閉全屏示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08Web?Animations?API實現(xiàn)一個精確計時的時鐘示例
這篇文章主要為大家介紹了Web?Animations?API實現(xiàn)一個精確計時的時鐘示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07