关于前端:谁动了我的-DOM

42次阅读

共计 11782 个字符,预计需要花费 30 分钟才能阅读完成。

在某些场景下,咱们心愿能监督 DOM 树的变动,而后做一些相干的操作。比方监听元素被插入 DOM 或从 DOM 树中移除,而后增加相应的动画成果。或者在富文本编辑器中输出非凡的符号,如 #@ 符号时主动高亮前面的内容等。要实现这些性能,咱们就能够思考应用 MutationObserver API,接下来阿宝哥将带大家一起来摸索 MutationObserver API 所提供的弱小能力。

浏览完本文,你将理解以下内容:

  • MutationObserver 是什么;
  • MutationObserver API 的根本应用及 MutationRecord 对象;
  • MutationObserver API 常见的应用场景;
  • 什么是观察者设计模式及如何应用 TS 实现观察者设计模式。

一、MutationObserver 是什么

MutationObserver 接口提供了监督对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 性能的替代品,该性能是 DOM3 Events 标准的一部分。

利用 MutationObserver API 咱们能够监督 DOM 的变动。DOM 的任何变动,比方节点的减少、缩小、属性的变动、文本内容的变动,通过这个 API 咱们都能够失去告诉。

MutationObserver 有以下特点:

  • 它期待所有脚本工作执行实现后,才会运行,它是异步触发的。即会期待以后所有 DOM 操作都完结才触发,这样设计是为了应答 DOM 频繁变动的问题。
  • 它把 DOM 变动记录封装成一个数组进行对立解决,而不是一条一条进行解决。
  • 它既能够察看 DOM 的所有类型变动,也能够指定只察看某一类变动。

二、MutationObserver API 简介

在介绍 MutationObserver API 之前,咱们先来理解一下它的兼容性:

(图片起源:https://caniuse.com/#search=M…)

从上图可知,目前支流的 Web 浏览器根本都反对 MutationObserver API,而对于 IE 浏览器只有 IE 11 才反对。在我的项目中,如须要应用 MutationObserver API,首先咱们须要创立 MutationObserver 对象,因而接下来咱们来介绍 MutationObserver 构造函数。

DOM 标准中的 MutationObserver 构造函数,用于创立并返回一个新的观察器,它会在触发指定 DOM 事件时,调用指定的回调函数。MutationObserver 对 DOM 的察看不会立刻启动,而必须先调用 observe() 办法来指定所要察看的 DOM 节点以及要响应哪些更改。

2.1 构造函数

MutationObserver 构造函数的语法为:

const observer = new MutationObserver(callback);

相干的参数阐明如下:

  • callback:一个回调函数,每当被指定的节点或子树有产生 DOM 变动时会被调用。该回调函数蕴含两个参数:一个是形容所有被触发改变的 MutationRecord 对象数组,另一个是调用该函数的 MutationObserver 对象。

应用示例

const observer = new MutationObserver(function (mutations, observer) {mutations.forEach(function(mutation) {console.log(mutation);
  });
});

2.2 办法

  • disconnect():阻止 MutationObserver 实例持续接管告诉,除非再次调用其 observe() 办法,否则该观察者对象蕴含的回调函数都不会再被调用。
  • observe(target[, options]):该办法用来启动监听,它承受两个参数。第一个参数,用于指定所要察看的 DOM 节点。第二个参数,是一个配置对象,用于指定所要察看的特定变动。

    const editor = document.querySelector('#editor');
    
    const options = {
      childList: true, // 监督 node 间接子节点的变动
      subtree: true, // 监督 node 所有后辈的变动
      attributes: true, // 监督 node 属性的变动
      characterData: true, // 监督指定指标节点或子节点树中节点所蕴含的字符数据的变动。attributeOldValue: true // 记录任何有改变的属性的旧值
    };
    
    observer.observe(article, options);
  • takeRecords():返回已检测到但尚未由观察者的回调函数解决的所有匹配 DOM 更改的列表,使变更队列放弃为空。此办法最常见的应用场景是 在断开观察者之前立刻获取所有未解决的更改记录,以便在进行观察者时能够解决任何未解决的更改

2.3 MutationRecord 对象

DOM 每次发生变化,就会生成一条变动记录,即 MutationRecord 实例。该实例蕴含了与变动相干的所有信息。Mutation Observer 对象解决的就是一个个 MutationRecord 实例所组成的数组。

MutationRecord 实例蕴含了变动相干的信息,含有以下属性:

  • type:变动的类型,值能够是 attributes、characterData 或 childList;
  • target:产生变动的 DOM 节点;
  • addedNodes:返回新增的 DOM 节点,如果没有节点被增加,则返回一个空的 NodeList;
  • removedNodes:返回移除的 DOM 节点,如果没有节点被移除,则返回一个空的 NodeList;
  • previousSibling:返回被增加或移除的节点之前的兄弟节点,如果没有则返回 null
  • nextSibling:返回被增加或移除的节点之后的兄弟节点,如果没有则返回 null
  • attributeName:返回被批改的属性的属性名,如果设置了 attributeFilter,则只返回预先指定的属性;
  • attributeNamespace:返回被批改属性的命名空间;
  • oldValue:变动前的值。这个属性只对 attributecharacterData 变动无效,如果产生 childList 变动,则返回 null

2.4 MutationObserver 应用示例

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DOM 变动观察器示例 </title>
    <style>
      .editor {border: 1px dashed grey; width: 400px; height: 300px;}
    </style>
  </head>
  <body>
    <h3> 阿宝哥:DOM 变动观察器(Mutation observer)</h3>
    <div contenteditable id="container" class="editor"> 大家好,我是阿宝哥!</div>

    <script>
      const containerEle = document.querySelector("#container");

      let observer = new MutationObserver((mutationRecords) => {console.log(mutationRecords); // 输入变动记录
      });

      observer.observe(containerEle, {
        subtree: true, // 监督 node 所有后辈的变动
        characterDataOldValue: true, // 记录任何有变动的属性的旧值
      });
    </script>
  </body>
</html>

以上代码胜利运行之后,阿宝哥对 id 为 container 的 div 容器中原始内容进行批改,即把 大家好,我是阿宝哥! 批改为 大家好,我。对于上述的批改,控制台将会输入 5 条变动记录,这里咱们来看一下最初一条变动记录:

MutationObserver 对象的 observe(target [, options]) 办法反对很多配置项,这里阿宝哥就不具体开展介绍了。

然而为了让刚接触 MutationObserver API 的小伙伴能更直观的感触每个配置项的作用,阿宝哥把 mutationobserver-api-guide 这篇文章中应用的在线示例对立提取进去,做了一下汇总与分类:

1、MutationObserver Example – childList:https://codepen.io/impressive…

2、MutationObserver Example – childList with subtree:https://codepen.io/impressive…

3、MutationObserver Example – Attributes:https://codepen.io/impressive…

4、MutationObserver Example – Attribute Filter:https://codepen.io/impressive…

5、MutationObserver Example – attributeFilter with subtree:https://codepen.io/impressive…

6、MutationObserver Example – characterData:https://codepen.io/impressive…

7、MutationObserver Example – characterData with subtree:https://codepen.io/impressive…

8、MutationObserver Example – Recording an Old Attribute Value:https://codepen.io/impressive…

9、MutationObserver Example – Recording old characterData:https://codepen.io/impressive…

10、MutationObserver Example – Multiple Changes for a Single Observer:https://codepen.io/impressive…

11、MutationObserver Example – Moving a Node Tree:https://codepen.io/impressive…

三、MutationObserver 应用场景

3.1 语法高亮

置信大家对语法高亮都不会生疏,平时在浏览各类技术文章时,都会遇到它。接下来,阿宝哥将跟大家介绍如何应用 MutationObserver API 和 Prism.js 这个库实现 JavaScript 和 CSS 语法高亮。

在看具体的实现代码前,咱们先来看一下以下 HTML 代码段未语法高亮和语法高亮的区别:

let htmlSnippet = ` 上面是一个 JavaScript 代码段:<pre class="language-javascript">
       <code> let greeting = "大家好,我是阿宝哥"; </code>
    </pre>
    <div> 另一个 CSS 代码段:</div>
       <div>
         <pre class="language-css">
            <code>#code-container {border: 1px dashed grey; padding: 5px;} </code>
         </pre>
    </div>
`

通过观察上图,咱们能够很直观地发现,有进行语法高亮的代码块浏览起来更加清晰易懂。上面咱们来看一下实现语法高亮的性能代码:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MutationObserver 实战之语法高亮 </title>
    <style>
      #code-container {
        border: 1px dashed grey;
        padding: 5px;
        width: 550px;
        height: 200px;
      }
    </style>
    <link href="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/themes/prism.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/prism.min.js" data-manual></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/components/prism-javascript.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/components/prism-css.min.js"></script>
  </head>
  <body>
    <h3> 阿宝哥:MutationObserver 实战之语法高亮 </h3>
    <div id="code-container"></div>
    <script>
      let observer = new MutationObserver((mutations) => {for (let mutation of mutations) {
          // 获取新增的 DOM 节点
          for (let node of mutation.addedNodes) {
            // 只解决 HTML 元素,跳过其余节点,比方文本节点
            if (!(node instanceof HTMLElement)) continue;

            // 查看插入的节点是否为代码段
            if (node.matches('pre[class*="language-"]')) {Prism.highlightElement(node);
            }

            // 查看插入节点的子节点是否为代码段
            for (let elem of node.querySelectorAll('pre[class*="language-"]')) {Prism.highlightElement(elem);
            }
          }
        }
      });

      let codeContainer = document.querySelector("#code-container");

      observer.observe(codeContainer, { childList: true, subtree: true});
      // 动静插入带有代码段的内容
      codeContainer.innerHTML = ` 上面是一个 JavaScript 代码段:<pre class="language-javascript"><code> let greeting = "大家好,我是阿宝哥"; </code></pre>
        <div> 另一个 CSS 代码段:</div>
        <div>
          <pre class="language-css">
             <code>#code-container {border: 1px dashed grey; padding: 5px;} </code>
          </pre>
        </div>
        `;
    </script>
  </body>
</html>

在以上代码中,首先咱们在引入 prism.min.js 的 script 标签上设置 data-manual 属性,用于通知 Prism.js 咱们将应用手动模式来解决语法高亮。接着咱们在回调函数中通过获取 mutation 对象的 addedNodes 属性来进一步获取新增的 DOM 节点。而后咱们遍历新增的 DOM 节点,判断新增的 DOM 节点是否为代码段,如果满足条件的话则进行高亮操作。

此外,除了判断以后节点之外,咱们也会判断插入节点的子节点是否为代码段,如果满足条件的话,也会进行高亮操作。

3.2 监听元素的 load 或 unload 事件

对 Web 开发者来说,置信很多人对 load 事件都不会生疏。当整个页面及所有依赖资源如样式表和图片都已实现加载时,将会触发 load 事件。而当文档或一个子资源正在被卸载时,会触发 unload 事件。

在日常开发过程中,除了监听页面的加载和卸载事件之外,咱们常常还须要监听 DOM 节点的插入和移除事件。比方当 DOM 节点插入 DOM 树中产生插入动画,而当节点从 DOM 树中被移除时产生移除动画。针对这种场景咱们就能够利用 MutationObserver API 来监听元素的增加与移除。

同样,在看具体的实现代码前,咱们先来看一下理论的成果:

在以上示例中,当点击 跟踪元素生命周期 按钮时,一个新的 DIV 元素会被插入到 body 中,胜利插入后,会在音讯框显示相干的信息。在 3S 之后,新增的 DIV 元素会从 DOM 中移除,胜利移除后,会在音讯框中显示 元素已从 DOM 中移除了 的信息。

上面咱们来看一下具体实现:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MutationObserver load/unload 事件 </title>
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.0.0/animate.min.css"
    />
  </head>
  <body>
    <h3> 阿宝哥:MutationObserver load/unload 事件 </h3>
    <div class="block">
      <p>
        <button onclick="trackElementLifecycle()"> 跟踪元素生命周期 </button>
      </p>
      <textarea id="messageContainer" rows="5" cols="50"></textarea>
    </div>
    <script src="./on-load.js"></script>
    <script>
      const busy = false;
      const messageContainer = document.querySelector("#messageContainer");

      function trackElementLifecycle() {if (busy) return;
        const div = document.createElement("div");
        div.innerText = "我是新增的 DIV 元素";
        div.classList.add("animate__animated", "animate__bounceInDown");
        watchElement(div);
        document.body.appendChild(div);
      }

      function watchElement(element) {
        onload(
          element,
          function (el) {
            messageContainer.value = "元素已被增加到 DOM 中, 3s 后将被移除";
            setTimeout(() => document.body.removeChild(el), 3000);
          },
          function (el) {messageContainer.value = "元素已从 DOM 中移除了";}
        );
      }
    </script>
  </body>
</html>

on-load.js

// 只蕴含局部代码
const watch = Object.create(null);
const KEY_ID = "onloadid" + Math.random().toString(36).slice(2);
const KEY_ATTR = "data-" + KEY_ID;
let INDEX = 0;

if (window && window.MutationObserver) {const observer = new MutationObserver(function (mutations) {if (Object.keys(watch).length < 1) return;
    for (let i = 0; i < mutations.length; i++) {if (mutations[i].attributeName === KEY_ATTR) {eachAttr(mutations[i], turnon, turnoff);
        continue;
      }
      eachMutation(mutations[i].removedNodes, function (index, el) {if (!document.documentElement.contains(el)) turnoff(index, el);
      });
      eachMutation(mutations[i].addedNodes, function (index, el) {if (document.documentElement.contains(el)) turnon(index, el);
      });
    }
  });

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeOldValue: true,
    attributeFilter: [KEY_ATTR],
  });
}

function onload(el, on, off, caller) {on = on || function () {};
  off = off || function () {};
  el.setAttribute(KEY_ATTR, "o" + INDEX);
  watch["o" + INDEX] = [on, off, 0, caller || onload.caller];
  INDEX += 1;
  return el;
}

on-load.js 的残缺代码:https://gist.github.com/semli…

3.3 富文本编辑器

除了后面两个利用场景,在富文本编辑器的场景,MutationObserver API 也有它的用武之地。比方咱们心愿在富文本编辑器中高亮 # 符号后的内容,这时候咱们就能够通过 MutationObserver API 来监听用户输出的内容,发现用户输出 # 时主动对输出的内容进行高亮解决。

这里阿宝哥基于 vue-hashtag-textarea 这个我的项目来演示一下上述的成果:

此外,MutationObserver API 在 Github 上的一个名为 Editor.js 的我的项目中也有利用。Editor.js 是一个 Block-Styled 编辑器,以 JSON 格局输入数据的富文本和媒体编辑器。它是齐全模块化的,由“块”组成,这意味着每个结构单元都是它本人的块(例如段落、题目、图像都是块),用户能够轻松地编写本人的插件来进一步扩大编辑器。

在 Editor.js 编辑器外部,它通过 MutationObserver API 来监听富文本框的内容异动,而后触发 change 事件,使得内部能够对变动进行响应和解决。上述的性能被封装到外部的 modificationsObserver.ts 模块,感兴趣的小伙伴能够浏览 modificationsObserver.ts 模块的代码。

当然利用 MutationObserver API 提供的弱小能力,咱们还能够有其余的利用场景,比方避免页面的水印元素被删除,从而防止无奈跟踪到“泄密”者,当然这并不是相对的平安,只是多加了一层防护措施。具体如何实现水印元素被删除,篇幅无限。这里阿宝哥不持续开展介绍了,大家能够参考掘金上“关上控制台也删不掉的元素,前端都吓尿了”这一篇文章。

至此 MutationObserver 变动观察者相干内容曾经介绍完了,既然讲到观察者,阿宝哥不由自主想再介绍一下观察者设计模式。

四、观察者设计模式

4.1 简介

观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会告诉所有的观察者对象,使得它们可能自动更新本人。

咱们能够应用日常生活中,期刊订阅的例子来形象地解释一下下面的概念。期刊订阅蕴含两个次要的角色:期刊出版方和订阅者,他们之间的关系如下:

  • 期刊出版方 —— 负责期刊的出版和发行工作。
  • 订阅者 —— 只需执行订阅操作,新版的期刊公布后,就会被动收到告诉,如果勾销订阅,当前就不会再收到告诉。

在观察者模式中也有两个次要角色:Subject(主题)和 Observer(观察者),它们别离对应例子中的期刊出版方和订阅者。接下来咱们来看张图,进一步加深对以上概念的了解。

4.2 模式构造

观察者模式蕴含以下角色:

  • Subject:主题类
  • Observer:观察者

4.3 观察者模式实战

4.3.1 定义 Observer 接口
interface Observer {notify: Function;}
4.3.2 创立 ConcreteObserver 观察者实现类
class ConcreteObserver implements Observer{constructor(private name: string) {}

    notify() {console.log(`${this.name} has been notified.`);
    }
}
4.3.3 创立 Subject 类
class Subject {private observers: Observer[] = [];

    public addObserver(observer: Observer): void {console.log(observer, "is pushed!");
      this.observers.push(observer);
    }

    public deleteObserver(observer: Observer): void {console.log("remove", observer);
      const n: number = this.observers.indexOf(observer);
      n != -1 && this.observers.splice(n, 1);
    }

    public notifyObservers(): void {console.log("notify all the observers", this.observers);
      this.observers.forEach(observer => observer.notify());
    }
}
4.3.4 应用示例
const subject: Subject = new Subject();
const semlinker = new ConcreteObserver("semlinker");
const kaquqo = new ConcreteObserver("kakuqo");
subject.addObserver(semlinker);
subject.addObserver(kaquqo);
subject.notifyObservers();

subject.deleteObserver(kaquqo);
subject.notifyObservers();

以上代码胜利运行后,控制台会输入以下后果:

[LOG]: {"name": "semlinker"},  is pushed! 
[LOG]: {"name": "kakuqo"},  is pushed! 
[LOG]: notify all the observers,  [{ "name": "semlinker"}, {"name": "kakuqo"} ] 
[LOG]: semlinker has been notified. 
[LOG]: kakuqo has been notified. 
[LOG]: remove,  {"name": "kakuqo"} 
[LOG]: notify all the observers,  [{ "name": "semlinker"} ] 
[LOG]: semlinker has been notified. 

通过观察以上的输入后果,当观察者被移除当前,后续的告诉就接管不到了。观察者模式反对简略的播送通信,可能主动告诉所有曾经订阅过的对象。但如果一个被观察者对象有很多的观察者的话,将所有的观察者都告诉到会破费很多工夫。 所以在理论我的项目中应用的话,大家须要留神以上的问题。

五、参考资源

  • MDN – MutationObserver
  • MDN – MutationRecord
  • JavaScript 规范参考教程 – MutationObserver
  • mutationobserver-api-guide
  • javascript.info-mutation-observer

六、举荐浏览

  • 了不起的 TypeScript 入门教程
  • 了不起的 Deno 入门篇
  • 了不起的 Deno 实战教程
  • 你不晓得的 Blob
  • 你不晓得的 WeakMap

正文完
 0