<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
MutationObserver 介面提供了監視對 DOM 樹所做更改的能力。它被設計為舊的 Mutation Events 功能的替代品,該功能是 DOM3 Events 規範的一部分。 - MDN
也就是說,當監視的 DOM
發生變動時 MutationObserver
將收到通知並觸發事先設定好的回撥函數。這個功能非常強大,意味著對於我們可以更加方便的動態操作 DOM
元素了。
你是否能聯想到某些業務場景呢?
像這樣的列表頁,由於文案和文章配圖數量的不同導致有多種不同的 ui 設計和排列方式,所以在前端對資料渲染的時候,要對列表每一項內容型別進行甄別。使用 MutationObserver
可以非常簡單的完成這個需求
MutationObserver
是一個建構函式,通過呼叫 MutationObserver 建構函式並傳入一個回撥函數來建立一個觀察 DOM
的範例
const observer = new MutationObserver(() => console.log('DOM 發生變化了~'));
回撥引數的兩個引數:
mutationRecords
:陣列佇列,記錄操作的結果observer
:與建構函式的返回值 全等,因為這個回撥函數是 非同步執行,所以也可以存取到外部的 observer
後文還會再詳細討論這兩個引數
新建立的 MutationObserver
範例不會關聯 DOM 的任何部分。要把這個 observer
與 DOM
關聯起來,需要使用 observe()
方法
observer.observe(document.body, { attributes: true });
這個方法接收必需的引數:
DOM
節點MutationObserverInit
物件這樣 document.body
就被觀察了,只要 document.body
元素的任何屬性值發生變化,就會觸發觀察物件,並且 非同步呼叫 傳入 MutationObserver
的回撥函數(這是一個 微任務)
const observer = new MutationObserver(() => console.log('DOM 發生變化了~')); observer.observe(document.body, { attributes: true }); setTimeout(() => { document.body.className = 'test'; }, 1000);
等過了一秒之後,定時器的回撥函數執行,修改了 document.body
的 class
屬性,所以了觸發 MutationObserver
的回撥函數
在上面的例子中,只要 document.body
本身的任意屬性發生了,都會被觀察到,但是其他修改 DOM
的行為不會被觀察,例如節點的增刪改查,子節點屬性的修改...,因為我們在呼叫 observe()
方法的時候傳入的 MutationObserverInit
物件新增了 attributes
屬性,所以 observe()
方法作用是隻能偵測自身的元素屬性值的變化。MutationObserverInit
物件除了這個屬性之外,還有很多非常強大的屬性可以觀察更多的節點操作
MutationObserverInit
物件用於控制對目標節點的觀察範圍。觀察方式的型別有 屬性變化、文字變化 和 子節點變化 這三種。
所以在呼叫 observe()
時,MutationObserverInit
物件中的 attribute
(屬性變化)、characterData
(文字變化) 和 childList
(子節點變化) 屬性必須 至少有一項 為 true
(無論是直接設定這幾個屬性,還是通過設定 attributeOldValue
(屬性變化)等屬性間接導致它們的值轉換為 true
)。否則會丟擲錯誤,因為 DOM
的變化不會被任何變化事件型別觸發回撥。
觀察節點 屬性 的 新增、移除 和 修改。需要在 MutationObserverInit
物件中將 attributes
屬性設定為 true
const observer = new MutationObserver(() => console.log('DOM 發生變化了~')); observer.observe(document.body, { attributes: true }); setTimeout(() => { document.body.className = 'test'; }, 1000);
還有 attributeOldValue: true
:可以記錄變化之前的屬性值。attributeFilter: ['class', 'id']
:可以觀察哪些屬性的變化,在這裡只觀察了 class
和 id
屬性
觀察文位元組點(如 Text 文位元組點、Comment 註釋 ) 中字元的 新增、刪除 和 修改。要在 MutationObserverInit
物件中將 characterData
屬性設定為 true
const observer = new MutationObserver(() => console.log('DOM 發生變化了~')); observer.observe(document.body.firstChild, { characterData: true }); setTimeout(() => { document.body.firstChild.textContent = '123'; }, 1000);
還有 characterDataOldValue
:可以記錄變化之前的文字值
觀察目標節點子節點的新增和移除。需要在 MutationObserverInit
物件中將 childList
屬性設定為 true
const observer = new MutationObserver(() => console.log('DOM 發生變化了~')); observer.observe(document.body, { childList: true }); setTimeout(() => { document.body.appendChild(document.createElement('div')); }, 1000);
在這個例子中控制檯輸出兩次,第一次是 body
元素在 0s
觸發回撥,第二次才是新建立的元素在 1s
之後觸發回撥,因為觀察 document.body
會在建立 body
的時候就立即被觀察到,而觀察非 body
元素,不會觸發自身建立的過程
childList
只會觀察子節點,但不會觀察深層的節點,可以在 MutationObserverInit
物件中將 subtree
屬性設定為 true
,還得將 childList
為 true
,因為 MutationObserverInit
物件中的 attribute
、characterData
和 childList
屬性必須 至少有一項 為 true
<div></div> <script> const observer = new MutationObserver(mutationRecords => { console.log('觸發了'); 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>
這裡雖然只會觸發一次回撥,但是會在 mutationRecords
這個陣列中會分別記下兩次 DOM
操作的記錄,所以陣列的長度為 2
<div></div> <script> const observer = new MutationObserver(mutationRecords => { console.log('觸發了'); 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>
這個例子與上個例子區別是將兩次 DOM
操作放在兩個不同的定時器執行,但是結果卻是截然不同,這裡會輸出兩次,mutationRecords
陣列的長度為 1
這是因為 DOM
操作是同步的,DOM
渲染是非同步的,MutationObserver
中的回撥函數執行會被包裹在一個 微任務 中,而定時器是 宏任務,所以整個執行過程是:第一個定時器先執行,觀察 DOM
的回撥函數執行,第二個定時器再執行,所以 DOM
變化被觀察了兩次。
上一個的例子 DOM
操作是在同一個 宏任務
中執行,因為瀏覽器會優化 DOM
渲染的過程,所以等到兩個 div
元素建立完畢才會渲染,之後執行觀察 DOM
的 微任務,所以才會觸發一次觀察,但是產生了兩個結果,所以 mutationRecords
陣列的長度為 2
這裡還有一個怪異現象,在第二個例子中,為什麼兩輸出 mutationRecords
的長度都是 1
,因為這兩個陣列不是同一個陣列,關於為什麼 mutationRecords
陣列 不會快取 第一次的操作結果,而是建立兩個不同的陣列,會在後面的內容詳細討論。
預設情況下,只要被觀察的元素不被垃圾回收,MutationObserver
的回撥就會響應 DOM
變化事件,從而被執行。想要 提前終止執行 回撥,可以呼叫 disconnect()
方法。
<div></div> <script> const observer = new MutationObserver(mutationRecords => { console.log('觸發了'); }); 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>
在這個例子中,在第一秒的時候執行了 DOM
操作,並且建立一個定時器包裹 disconnect()
方法,然後執行 disconnect()
方法,在第二秒的時候執行了另外一個 DOM
操作。所以結果只有第一次 DOM
操作會被觀察到
為什麼這裡需要將 disconnect
方法計時器裡執行呢,千萬別忘了,DOM
操作是 同步執行 的,DOM
渲染是 非同步執行 的,disconnect()
也是 同步執行 的。如果不新增定時器,在 DOM
渲染值之前就取消了觀察,雖然操作了 DOM
,但是渲染過程並沒有觀察到
呼叫 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
,所以在呼叫第一次 takeRecords()
方法的時候會輸出 3
,並且切斷了與觀察物件的聯絡,所以不會觸發 MutationObserver
的回撥,但是這種切斷關係是 不牢靠 的,也就意味著下次的 DOM
操作會 重啟觀察,就像下面的這個例子表現的一樣
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'; });
MutationRecord
是一個 記錄佇列 的陣列,,僅當 微任務佇列 沒有其他的微任務回撥時(佇列中微任務 長度為 0
),才會將觀察者註冊的 回撥 作為微任務放置到任務佇列上。這樣可以保證記錄佇列的內容不會被回撥處理兩次。
在回撥的微任務非同步執行期間,有可能又會發生更多變化事件。因此被呼叫的回撥會接收到一個 MutationRecord
範例的陣列,順序為它們進入記錄佇列的順序。回撥要負責處理這個陣列的每一個範例,因為 回撥函數 退出之後這些實現就不存在了。回撥函數執行完成後,這些 MutationRecord
就用不著了, 因此記錄佇列會被清空,其內容會被丟棄。所以每一個回撥函數中的 MutationRecords
陣列是 不同的範例
const observer = new MutationObserver(mutationRecords => { console.log(mutationRecords); }); const oDiv = document.getElementsByTagName('div')[0]; observer.observe(oDiv, { attributeOldValue: true }); oDiv.classList.add('box');
幾個重要的屬性:
屬性 | 說明 |
---|---|
target | 被修改影響的目標節點 |
type | 表示變化的型別:"attributes"、"characterData"或"childList" |
oldValue | 如果在 MutationObserverInit 物件中啟用(attributeOldValue 或 characterData OldValue 為 true),"attributes"或"characterData"的變化事件會設定這個屬性為被替代的值 "childList"型別的變化始終將這個屬性設定為 null |
addedNodes | 對於"childList"型別的變化,返回包含變化中新增節點的 NodeList 預設為空 NodeList |
一個簡單的業務場景:
使用者提交評論,如果評論的內容超過最大寬度,需要隱藏多餘的部分,同時展示“檢視更多”按鈕,點選這個按鈕就會展示評論的全部內容
難點:只有當 DOM
被渲染的時候 才知道實際的高度,所以無法預先分析評論文字內容而選擇渲染方式的型別
實現思路:使用 MutationObserver
監聽評論區列表,每當使用者提交新的評論,新生成的 DOM
就會被觀察到,判斷評論的內容是否超出最大高度,更新 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; // 新新增的節點 // 新增加的按鈕也會觸發觀察,所以要判斷新增加節點是否是評論 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), // 解析輸入文字內容 showBtn: false, // 預設不超出最大高度 }); }; const parseComment = (str: string) => { return str.replace(/[nr]/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[]>([]); // 迴圈繫結 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 高階程式設計第 4 版.PDF – 1024.Cool
MutationObserver
api 使用大多數場景為:動態監聽 DOM
元素的變化,在傳入建構函式的回撥函數中可以存取到觸發 DOM
變化的 target
和 影響 DOM
變化的結果
以上就是Dom-api MutationObserver使用方法詳解的詳細內容,更多關於Dom-api MutationObserver方法的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45