|
| 1 | +# Mutation Observer API |
| 2 | + |
| 3 | +## 概述 |
| 4 | + |
| 5 | +Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。 |
| 6 | + |
| 7 | +概念上,它很接近事件,可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。 |
| 8 | + |
| 9 | +这样设计是为了应付 DOM 变动频繁的特点。举例来说,如果文档中连续插入1000个`<p>`元素,就会连续触发1000个插入事件,执行每个事件的回调函数,这很可能造成浏览器的卡顿;而 Mutation Observer 完全不同,只在1000个段落都插入结束后才会触发,而且只触发一次。 |
| 10 | + |
| 11 | +Mutation Observer 有以下特点。 |
| 12 | + |
| 13 | +- 它等待所有脚本任务完成后,才会运行(即异步触发方式)。 |
| 14 | +- 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。 |
| 15 | +- 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。 |
| 16 | + |
| 17 | +## MutationObserver 构造函数 |
| 18 | + |
| 19 | +使用时,首先使用`MutationObserver`构造函数,新建一个观察器实例,同时指定这个实例的回调函数。 |
| 20 | + |
| 21 | +```javascript |
| 22 | +var observer = new MutationObserver(callback); |
| 23 | +``` |
| 24 | + |
| 25 | +上面代码中的回调函数,会在每次 DOM 变动后调用。该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例,下面是一个例子。 |
| 26 | + |
| 27 | +```javascript |
| 28 | +var observer = new MutationObserver(function (mutations, observer) { |
| 29 | + mutations.forEach(function(mutation) { |
| 30 | + console.log(mutation); |
| 31 | + }); |
| 32 | +}); |
| 33 | +``` |
| 34 | + |
| 35 | +## MutationObserver 的实例方法 |
| 36 | + |
| 37 | +### observe() |
| 38 | + |
| 39 | +`observe`方法用来启动监听,它接受两个参数。 |
| 40 | + |
| 41 | +- 第一个参数:所要观察的 DOM 节点 |
| 42 | +- 第二个参数:一个配置对象,指定所要观察的特定变动 |
| 43 | + |
| 44 | +```javascript |
| 45 | +var article = document.querySelector('article'); |
| 46 | + |
| 47 | +var options = { |
| 48 | + 'childList': true, |
| 49 | + 'attributes':true |
| 50 | +} ; |
| 51 | + |
| 52 | +observer.observe(article, options); |
| 53 | +``` |
| 54 | + |
| 55 | +上面代码中,`observe`方法接受两个参数,第一个是所要观察的DOM元素是`article`,第二个是所要观察的变动类型(子节点变动和属性变动)。 |
| 56 | + |
| 57 | +观察器所能观察的 DOM 变动类型(即上面代码的`options`对象),有以下几种。 |
| 58 | + |
| 59 | +- **childList**:子节点的变动。 |
| 60 | +- **attributes**:属性的变动。 |
| 61 | +- **characterData**:节点内容或节点文本的变动。 |
| 62 | +- **subtree**:所有后代节点的变动。 |
| 63 | + |
| 64 | +想要观察哪一种变动类型,就在`option`对象中指定它的值为`true`。需要注意的是,如果设置观察`subtree`的变动,必须同时指定`childList`、`attributes`和`characterData`中的一种或多种。 |
| 65 | + |
| 66 | +除了变动类型,`options`对象还可以设定以下属性: |
| 67 | + |
| 68 | +- `attributeOldValue`:布尔值,表示观察`attributes`变动时,是否需要记录变动前的属性值。 |
| 69 | +- `characterDataOldValue`:布尔值,表示观察`characterData`变动时,是否需要记录变动前的值。 |
| 70 | +- `attributeFilter`:数组,表示需要观察的特定属性(比如`['class','src']`)。 |
| 71 | + |
| 72 | +```javascript |
| 73 | +// 开始监听文档根节点(即<html>标签)的变动 |
| 74 | +mutationObserver.observe(document.documentElement, { |
| 75 | + attributes: true, |
| 76 | + characterData: true, |
| 77 | + childList: true, |
| 78 | + subtree: true, |
| 79 | + attributeOldValue: true, |
| 80 | + characterDataOldValue: true |
| 81 | +}); |
| 82 | +``` |
| 83 | + |
| 84 | +对一个节点添加观察器,就像使用`addEventListener`方法一样,多次添加同一个观察器是无效的,回调函数依然只会触发一次。但是,如果指定不同的`options`对象,就会被当作两个不同的观察器。 |
| 85 | + |
| 86 | +下面的例子是观察新增的子节点。 |
| 87 | + |
| 88 | +```javascript |
| 89 | +var insertedNodes = []; |
| 90 | +var observer = new MutationObserver(function(mutations) { |
| 91 | + mutations.forEach(function(mutation) { |
| 92 | + for (var i = 0; i < mutation.addedNodes.length; i++) |
| 93 | + insertedNodes.push(mutation.addedNodes[i]); |
| 94 | + }) |
| 95 | +}); |
| 96 | +observer.observe(document, { childList: true }); |
| 97 | +console.log(insertedNodes); |
| 98 | +``` |
| 99 | + |
| 100 | +### disconnect(),takeRecords() |
| 101 | + |
| 102 | +`disconnect`方法用来停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器。 |
| 103 | + |
| 104 | +```javascript |
| 105 | +observer.disconnect(); |
| 106 | +``` |
| 107 | + |
| 108 | +`takeRecords`方法用来清除变动记录,即不再处理未处理的变动。该方法返回变动记录的数组。 |
| 109 | + |
| 110 | +```javascript |
| 111 | +observer.takeRecords(); |
| 112 | +``` |
| 113 | + |
| 114 | +下面是一个例子。 |
| 115 | + |
| 116 | +```javascript |
| 117 | +// 保存所有没有被观察器处理的变动 |
| 118 | +var changes = mutationObserver.takeRecords(); |
| 119 | + |
| 120 | +// 停止观察 |
| 121 | +mutationObserver.disconnect(); |
| 122 | +``` |
| 123 | + |
| 124 | +## MutationRecord 对象 |
| 125 | + |
| 126 | +DOM 每次发生变化,就会生成一条变动记录(MutationRecord 实例)。该实例包含了与变动相关的所有信息。Mutation Observer 处理的就是一个个`MutationRecord`实例所组成的数组。 |
| 127 | + |
| 128 | +`MutationRecord`对象包含了DOM的相关信息,有如下属性: |
| 129 | + |
| 130 | +- `type`:观察的变动类型(`attribute`、`characterData`或者`childList`)。 |
| 131 | +- `target`:发生变动的DOM节点。 |
| 132 | +- `addedNodes`:新增的DOM节点。 |
| 133 | +- `removedNodes`:删除的DOM节点。 |
| 134 | +- `previousSibling`:前一个同级节点,如果没有则返回`null`。 |
| 135 | +- `nextSibling`:下一个同级节点,如果没有则返回`null`。 |
| 136 | +- `attributeName`:发生变动的属性。如果设置了`attributeFilter`,则只返回预先指定的属性。 |
| 137 | +- `oldValue`:变动前的值。这个属性只对`attribute`和`characterData`变动有效,如果发生`childList`变动,则返回`null`。 |
| 138 | + |
| 139 | +## 应用示例 |
| 140 | + |
| 141 | +### 子元素的变动 |
| 142 | + |
| 143 | +下面的例子说明如何读取变动记录。 |
| 144 | + |
| 145 | +```javascript |
| 146 | +var callback = function (records){ |
| 147 | + records.map(function(record){ |
| 148 | + console.log('Mutation type: ' + record.type); |
| 149 | + console.log('Mutation target: ' + record.target); |
| 150 | + }); |
| 151 | +}; |
| 152 | + |
| 153 | +var mo = new MutationObserver(callback); |
| 154 | + |
| 155 | +var option = { |
| 156 | + 'childList': true, |
| 157 | + 'subtree': true |
| 158 | +}; |
| 159 | + |
| 160 | +mo.observe(document.body, option); |
| 161 | +``` |
| 162 | + |
| 163 | +上面代码的观察器,观察`<body>`的所有下级节点(`childList`表示观察子节点,`subtree`表示观察后代节点)的变动。回调函数会在控制台显示所有变动的类型和目标节点。 |
| 164 | + |
| 165 | +### 属性的变动 |
| 166 | + |
| 167 | +下面的例子说明如何追踪属性的变动。 |
| 168 | + |
| 169 | +```javascript |
| 170 | +var callback = function (records) { |
| 171 | + records.map(function (record) { |
| 172 | + console.log('Previous attribute value: ' + record.oldValue); |
| 173 | + }); |
| 174 | +}; |
| 175 | + |
| 176 | +var mo = new MutationObserver(callback); |
| 177 | + |
| 178 | +var element = document.getElementById('#my_element'); |
| 179 | + |
| 180 | +var options = { |
| 181 | + 'attributes': true, |
| 182 | + 'attributeOldValue': true |
| 183 | +} |
| 184 | + |
| 185 | +mo.observe(element, options); |
| 186 | +``` |
| 187 | + |
| 188 | +上面代码先设定追踪属性变动(`'attributes': true`),然后设定记录变动前的值。实际发生变动时,会将变动前的值显示在控制台。 |
| 189 | + |
| 190 | +### 取代 DOMContentLoaded 事件 |
| 191 | + |
| 192 | +网页加载的时候,DOM 节点的生成会产生变动记录,因此只要观察 DOM 的变动,就能在第一时间触发相关事件,因此也就没有必要使用`DOMContentLoaded`事件。 |
| 193 | + |
| 194 | +```javascript |
| 195 | +var observer = new MutationObserver(callback); |
| 196 | +observer.observe(document.documentElement, { |
| 197 | + childList: true, |
| 198 | + subtree: true |
| 199 | +}); |
| 200 | +``` |
| 201 | + |
| 202 | +上面代码中,监听`document.documentElement`(即HTML节点)的子节点的变动,`subtree`属性指定监听还包括后代节点。因此,任意一个网页元素一旦生成,就能立刻被监听到。 |
| 203 | + |
| 204 | +下面的代码,使用`MutationObserver`对象封装一个监听 DOM 生成的函数。 |
| 205 | + |
| 206 | +```javascript |
| 207 | +(function(win){ |
| 208 | + 'use strict'; |
| 209 | + |
| 210 | + var listeners = []; |
| 211 | + var doc = win.document; |
| 212 | + var MutationObserver = win.MutationObserver || win.WebKitMutationObserver; |
| 213 | + var observer; |
| 214 | + |
| 215 | + function ready(selector, fn){ |
| 216 | + // 储存选择器和回调函数 |
| 217 | + listeners.push({ |
| 218 | + selector: selector, |
| 219 | + fn: fn |
| 220 | + }); |
| 221 | + if(!observer){ |
| 222 | + // 监听document变化 |
| 223 | + observer = new MutationObserver(check); |
| 224 | + observer.observe(doc.documentElement, { |
| 225 | + childList: true, |
| 226 | + subtree: true |
| 227 | + }); |
| 228 | + } |
| 229 | + // 检查该节点是否已经在DOM中 |
| 230 | + check(); |
| 231 | + } |
| 232 | + |
| 233 | + function check(){ |
| 234 | + // 检查是否匹配已储存的节点 |
| 235 | + for(var i = 0; i < listeners.length; i++){ |
| 236 | + var listener = listeners[i]; |
| 237 | + // 检查指定节点是否有匹配 |
| 238 | + var elements = doc.querySelectorAll(listener.selector); |
| 239 | + for(var j = 0; j < elements.length; j++){ |
| 240 | + var element = elements[j]; |
| 241 | + // 确保回调函数只会对该元素调用一次 |
| 242 | + if(!element.ready){ |
| 243 | + element.ready = true; |
| 244 | + // 对该节点调用回调函数 |
| 245 | + listener.fn.call(element, element); |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + // 对外暴露ready |
| 252 | + win.ready = ready; |
| 253 | + |
| 254 | +})(this); |
| 255 | + |
| 256 | +ready('.foo', function(element){ |
| 257 | + // ... |
| 258 | +}); |
| 259 | +``` |
| 260 | + |
| 261 | +## 参考链接 |
| 262 | + |
| 263 | +- Paul Kinlan, [Detect DOM changes with Mutation Observers](https://developers.google.com/web/updates/2012/02/Detect-DOM-changes-with-Mutation-Observers) |
| 264 | +- Tiffany Brown, [Getting to know mutation observers](http://dev.opera.com/articles/view/mutation-observers-tutorial/) |
| 265 | +- Michal Budzynski, [JavaScript: The less known parts. DOM Mutations](http://michalbe.blogspot.com/2013/04/javascript-less-known-parts-dom.html) |
| 266 | +- Jeff Griffiths, [DOM MutationObserver – reacting to DOM changes without killing browser performance](https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/) |
| 267 | +- Addy Osmani, [Detect, Undo And Redo DOM Changes With Mutation Observers](http://addyosmani.com/blog/mutation-observers/) |
| 268 | +- Ryan Morr, [Using Mutation Observers to Watch for Element Availability](http://ryanmorr.com/using-mutation-observers-to-watch-for-element-availability/) |
0 commit comments