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