观察范围
上一节,我们使用MutationObserver
时,都只是观察节点的属性。但是实际上并不仅仅是只能观察节点的属性,还可以观察子节点、子树等。只需要调用observe()
方法时,第二个参数添加对应配置即可。
属性 | 说明 |
---|---|
attributes |
布尔值,表示观察目标节点的属性变化 |
attributeFilter |
字符串数组,表示要观察哪些属性的变化。(类似白名单,只有白名单的才会被观察) |
attributeOldValue |
布尔值,表示MutationRecord 是否记录变化之间的数据。设置该属性为true ,会将attributes 的值转换为true 。 |
characterData |
布尔值,表示观察文本节点。 |
characterDataOldValue |
布尔值,表示MutationRecord 是否记录变化之间的数据。和attributeOldValue 一样,对应characterData |
childList |
布尔值,表示观察子节点 |
subtree |
布尔值。表示观察目标节点及其子树。如果为false ,则之观察目标节点的变化,为true |
观察属性
观察属性就是上一节一直在用的。
const observer = new MutationObserver((mutationsRecords) => {
console.log(mutationsRecords)
})
observer.observe(document.body, {
attributes: true
})
document.body.setAttribute('name', 'clz')
如果我们不需要观察所有属性,而只是观察某个或某几个属性,可以使用 attributeFilter
属性来设置白名单,值是一个属性名数组。
let observer = new MutationObserver((mutationRecords) => {
console.log(mutationRecords)
})
observer.observe(document.body, {
attributeFilter: ['name', 'age']
})
document.body.setAttribute('name', 'clz')
document.body.setAttribute('age', 21)
document.body.setAttribute('job', 'FontEnd-Coder')
上面设置了name
和age
为白名单,即只观察name
和age
属性,所以后面设置job
属性不会触发回调。
从上图,我们可以看到一个oldValue属性,它就是用来保存属性原来的值的。而默认是不会保存属性原来的值的,如果想要记录原来的值,可以将 attributeOldValue
属性设置为 true
。设置该属性为true
,会将attributes
的值转换为true
。
const observer = new MutationObserver((mutationRecords) => {
mutationRecords.map(mutationRecord => console.log(mutationRecord.oldValue))
})
observer.observe(document.body, {
attributeOldValue: true
})
document.body.setAttribute('name', 'clz')
document.body.setAttribute('name', 'czh')
设置name
属性为clz
的时候打印原来的值,原来没有值,所以打印null
,设置为czh
的时候打印原来的值czh
。
观察文本节点
MutationObserver
可以观察文本节点。
const observer = new MutationObserver((mutationRecords) => {
console.log(mutationRecords)
})
document.body.firstChild.textContent = 'hello'
observer.observe(document.body.firstChild, {
characterData: true
})
document.body.firstChild.textContent = '123'
document.body.firstChild.textContent = '456'
document.body.firstChild.textContent = '789'
如果想要记录原来的值,可以将 characterDataOldValue
属性设置为 true
。设置该属性为true
,会将characterData
的值转换为true
。
const observer = new MutationObserver((mutationRecords) => {
mutationRecords.map(mutationRecord => console.log(mutationRecord.oldValue))
})
document.body.firstChild.textContent = 'clz'
observer.observe(document.body.firstChild, {
characterDataOldValue: true
})
document.body.firstChild.textContent = '123'
document.body.firstChild.textContent = '456'
document.body.firstChild.textContent = '789'
注意:innerText
和textContent
有点点相似,但是innerText
可能会引发一些问题。
首先,innerText
是元素节点的属性,表示一个节点及其后代的“渲染”文本内容。而textContent
是节点的属性,表示节点的一个节点及其后代的文本内容。
举个小例子,说明他们两的区别。
<body>
<div>
<span>
123
</span>
<span style="display:none">
456
</span>
</div>
<script>
const div = document.querySelector('div')
console.log(div.innerText)
console.log(div.textContent)
console.log('%c%s', 'color:red;font-size:24px', '============')
const divChild = div.firstChild
console.log(divChild.textContent)
console.log(divChild.innerText)
divChild.textContent = '456' // 会在span节点前添加上456
// divChild.innerText = '456' // 没有效果,因为文本节点没有innerText属性
</script>
</body>
差异:
innerText
属性不会获取display
为none
的隐藏元素,而textContent
会获取。innerText
没有格式,而textContent
有格式- 文本节点没有
innerText
属性
从上面可以看到,innerText
属性不会获取display
为none
的隐藏元素,而textContent
会获取。也就是说,innetText
属性值的获取会触发回流,因为它需要考虑到CSS样式(如display
),而textContent
只是单纯读取文本内容,所以不会发生回流。
当我们观察节点时修改的是innerText
,而不是textContent
的话,会引发不一样的情况(个人认为算bug了,如果有了解原因的小伙伴,可以评论交流)
另外红宝书不建议使用innerText
,但是,明知山有虎,偏向虎山行。(了解使用后会有什么隐患)
const observer = new MutationObserver(
(mutationRecords) => mutationRecords.map((x) => console.log(x.oldValue))
);
document.body.innerText = 'clz'
observer.observe(document.body.firstChild, { characterDataOldValue: true });
document.body.firstChild.textContent = '123'
document.body.firstChild.textContent = '789'
- 观察前设置的
innerText
值也能被观察到 oldValue
不再是旧值,而是设置的新值
上面开始观察后,使用的是textContent
,因为使用innerText
又会导致另一个bug发生。
const observer = new MutationObserver(
(mutationRecords) => mutationRecords.map((x) => console.log(x.oldValue))
);
document.body.innerText = 'clz'
observer.observe(document.body.firstChild, { characterDataOldValue: true });
document.body.firstChild.textContent = '123'
document.body.innerText = '456'
document.body.firstChild.textContent = '789'
- 开始观察后,修改
innerText
属性会导致观察失效。包括开始观察后innerText
之前和之后的。
即使不混用,也还是有问题。
const observer = new MutationObserver(
(mutationRecords) => mutationRecords.map((x) => console.log(x.oldValue))
);
document.body.innerText = 'clz'
observer.observe(document.body.firstChild, { characterDataOldValue: true });
document.body.innerText = '123'
document.body.innerText = '456'
document.body.innerText = '789'
上面的代码不会打印任何东西。所以尽可能不要使用innerText
,而是使用textContent
。
观察子节点
MutationObserver
还可以观察目标节点子节点的添加和移除,只需要将childList
属性设置为true
即可。
<body>
<div id="box"></div>
<script>
const box = document.getElementById('box')
const observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords)
);
observer.observe(box, { childList: true })
box.appendChild(document.createElement('span')) // 在MutationRecord的addedNodes属性中可以查看到添加的节点
box.innerHTML = '<div></div>' // 使用innetHTML还会移除节点,表现为removedNodes中有被移除的节点
</script>
</body>
交换子节点顺序会导致发生两次变化,因为交换子节点顺序实际上有两个步骤,第一次是节点被移除,第二次是节点被添加。
<body>
<div id="box">
<b>1</b>
<span>2</span>
</div>
<script>
const box = document.getElementById('box')
const observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords)
);
observer.observe(box, { childList: true })
// box.insertBefore(box.firstElementChild, box.lastElementChild) // 即使最后顺序并没有发生改变,实际也是被移除后,再次插入原来的位置
box.insertBefore(box.lastElementChild, box.firstElementChild)
</script>
</body>
观察子树
MutationObserver
可以观察子树,只需要将subtree
属性设置为true
即可。
<body>
<div id="box">
<div>1</div>
<span>2</span>
</div>
<script>
const box = document.getElementById('box')
const observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords)
);
observer.observe(box, {
attributes: true,
subtree: true
});
box.firstElementChild.setAttribute('haha', 'haha')
box.firstElementChild.appendChild(document.createElement('b'))
</script>
</body>
但是,从上面,我们可以发现,只有修改属性才会被观察到,添加节点时并没有被观察到,那是不是观察子树不能观察节点的添加和移除呢?
并不是,这里只是因为分工明确,subtree
观察子树(不包括节点的添加和删除),childList
观察子节点,所以需要同时实现的话,那就需要两个属性都有。
const box = document.getElementById('box')
const observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords)
);
observer.observe(box, {
attributes: true,
subtree: true,
childList: true
});
box.firstElementChild.setAttribute('haha', 'haha')
box.firstElementChild.appendChild(document.createElement('b'))