优秀的编程知识分享平台

网站首页 > 技术文章 正文

第59节 CompositionEvent、MutationEvent及MutationObserver

nanyue 2024-08-05 20:02:39 技术文章 7 ℃

本内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。

CompositionEvent复合事件:

复合事件(composition event)是DOM3级上中新添加的一类事件,用于处理IME的输入序列;

IME(Input Method Editor,输入法编辑器)可以让用户输入在物理键盘上找不到的字符,例如,使用拉丁文键盘的用户通过IME可以输入其他语言字符,如日文字符;IME通常需要同时按住多个键,但最终只输入一个字符;

txt.addEventListener("compositionstart", function(event){
    console.log(event);  // CompositionEvent
});

CompositionEvent继承自UIEvent类,其定义了两个属性:

  • data属性,只读,返回正在编辑的原始字符串, 否则为空字符串;
  • locale属性,只读,组合事件的语言代码 (如果可用),否则, 为空字符串;除了IE其它浏览器都未实现;

要确定浏览器是否支持复合事件,可以检测:

var isSupported = document.implementation.hasFeature("CompositionEvent","3.0");

有三个复合事件:

compositionstart事件:

在IME的文本复合系统打开时触发,表示要开始输入了;冒泡,可以取消;此时,其data属性为空字符串;

txt.addEventListener("compositionstart", function(event){
    console.log(event);  // CompositionEvent
});

compositionupdate事件:

触发于字符被输入到一段文字的时候(这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)冒泡,不可取消;

其data属性为要被替换掉的字符串,如果输入时没有字符串被选,则为空字符串;

txt.addEventListener("compositionupdate", function(event){
    console.log(event.type + ":" + event.data);
});

其在输入一个汉字时,会被触发两次以上,前几次data属性为按键所对应的英文,最后一次为汉字;也就是compositionupdate 事件在编辑器里的内容改变之前就会触发;

compositionend:在IME的文本复合系统关闭时触发,表示返回正常键盘输入状态;(具有特殊字符的触发, 需要一系列键和其他输入, 如语音识别或移动中的字词建议);冒泡,可以取消;

txt.addEventListener("compositionend", function(event){
    console.log(event);  // CompositionEvent
    console.log(event.data);  // 汉字
    console.log(event.locale);  // undefined
});

复合事件与文本事件在很多方面都很相似,在触发复合事件时,目标是接收文本的输入字段,但它比文本事件的事件对象多一个data,其中包含以下几个值中的一个:

  • 如果在compositionstrt事件发生时访问,包含正在编辑的文本(例如,已经选中的需要马上替换的文本);
  • 如果在compositionupdate事件发生时访问,包含正插入的新字符;
  • 如果在compositionend事件发生时访问,包含此次输入会话中插入的所有字符;

和文本事件一样,必要时可以利用复合事件来筛选输入;

键盘及InputEvent事件中,也有个isComposing属性,只读属性,返回一个 Boolean 值,表示该事件是否在 compositionstart 之后和 compositionend 之前被触发,如:

txt.addEventListener("keydown", function(event){
    console.log(event.isComposing);
    console.log(event.keyCode);
})

在输入非中文时,isComposing为false,当为汉字时,其为true,keyCode为229;

txt.addEventListener("keydown", function(event){
    if(event.isComposing || event.keyCode === 229)
        return;
    // do something
})

MutationEvent变动事件:

DOM2级的变动(mutation)事件能在DOM中的某一部分发生变化时给出提示;变动事件是为XML或HTML DOM设计的,并不特定于某种语言;

DOM2级定义了如下变动事件:

  • DOMAttrModified:在特性被修改之后触发;
  • DOMSubtreeModified : 当文档或者元素的子树因为添加或者删除节点而发生的任何改变时触发,该事件在其他任何事件触发后都会触发;
  • DOMNodeInserted : 当一个节点作为子节点被插入到另一个节点时触发;
  • DOMNodeInsertedIntoDocument : 当一个节点被直接插入到文档或通过子树间接接入文档之后触发,该事件在DOMNodeInserted之后触发;
  • DOMNodeRemoved : 当节点从其父节点中被删除时触发;
  • DOMNodeRemovedFromDocument : 当一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发,该事件在DOMNodeRemoved之后触发;
  • DOMCharacterDataModified:在文本节点的值发生变化时触发;

可以使用如下代码检测浏览器是是否支持变动事件:

var isSupported = document.implementation.hasFeature("MutationEvents","2.0");

不是所有浏览器都支持所有事件;即使不支持所有事件,某些浏览器会返回true或false,所以这样检测还是不可靠;IE不支持任何变动事件;

节点移除:

使用removeChild()或replaceChild()从DOM中移除节点时,首先会触发DomNodeRemoved事件;该事件目标(event.target)是被移除的节点,而event.relatedNode属性中包含着对目标节点父节点的引用;在这个事件触发前,节点尚未从其父节点移除,因此其parentNode属性仍然指向父节点(与event.relatedNode相同);该事件会冒泡,因此可以在DOM的任何层次上都可以处理它;

如果被移除的节点包含子节点,那么在其所有子节点以及这个被移除的节点上会相继触发DOMNodeRemovedFromDocument事件;该事件不会冒泡,所以只有直接指定给其中一个子节点的事件处理程序才会被调用;此事件的目标是相应的子节点或者那个被移除的节点,除此之外event对象中不包含其他信息;

最后会触发DOMSubtreeModified事件;该事件的目标是被移除节点的父节点;此时event对象也不会提供与事件相关的其他信息;

<ul id="myList">
    <li>HTML</li>
    <li>CSS</li>
    <li>JavaScript</li>
</ul>
<script>
window.addEventListener("load", function(event){
    var list = document.getElementById("myList");
    document.addEventListener("DOMSubtreeModified", function(event){
        console.log(event.type);
        console.log(event.target);
    },false);
    document.addEventListener("DOMNodeRemoved", function(event){
        console.log(event.type);
        console.log(event.target);
        console.log(event.relatedNode);
    },false);
    document.addEventListener("DOMNodeRemovedFromDocument", function(event){
        console.log(event.type);
        console.log(event.target);
    },false);
    list.parentNode.removeChild(list);
},false);
</script>

节点插入:

在使用appendChild()、replaceChild()或insertBefore()向DOM中插入节点时,首先会触发DOMNodeinserted事件,该事件目标是被插入的节点,而event.relatedNode属性中包含一个对父节点的引用;在这个事件发生时,节点已经被插入到了新的父节点中;该事件是冒泡的,因此可以在DOM的各个层次上处理它;

紧接着,会在新插入的节点上面触发DomNodeInsertedIntoDocument事件;该事件不冒泡,因此必须在插入节点之前为它添加这个事件处理程序;该事件的目标是被插入的节点,除此之外event不包含其他信息

最后一个触发的事件是DOMSubtreeModified,由新插入节点的父节点触发;

window.addEventListener("load", function(event){
    var list = document.getElementById("myList");
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("大师哥王唯"));
    document.addEventListener("DOMSubtreeModified", function(event){
        console.log(event.type);
        console.log(event.target);
    },false);
    document.addEventListener("DOMNodeInserted", function(event){
        console.log(event.type);
        console.log(event.target);
        console.log(event.relatedNode);
    },false);
    item.addEventListener("DOMNodeInsertedIntoDocument", function(event){
        console.log(event.type);
        console.log(event.target);
    },false);
    list.appendChild(item);
},false);

特性变化:特性的值被修改时,会触发DOMAttrModified事件;该事件的目标是包含被修改特性的元素,而relatedNode表示被修改特性的Attr节点;其event对象提供了以下4个属性;

  • attrName:被修改的特性的名称;
  • attrChange:表示变化类型的值:1表示修改、2表示添加、3表示移除;
  • prevValue:被修改前的值(如果attrChange为2,此属性为空字符串);
  • newValue:被修改后的值(如果attrChange为3,此属性为空字符串);

该事件会冒泡,且紧跟会在被修改特性会触发DOMSubtreeModified事件;

window.addEventListener("load", function(event){
    var list = document.getElementById("myList");
    document.addEventListener("DOMSubtreeModified", function(event){
        console.log(event.type);
        console.log(event.target);
    },false);
    document.addEventListener("DOMAttrModified", function(event){
        console.log(event.type);
        console.log(event.target);
        console.log(event.relatedNode);
        console.log(event.attrName);
        console.log(event.attrChange);
        console.log(event.prevValue);
        console.log(event.newValue);
    },false);
    list.setAttribute("customname", "value");
},false);

文本变化:在文本节点发生变化时,会触发DOMCharacterDataModified事件;该事件的目标为这个文本节点;相应的event对象包含两个属性:prevValue和newValue;此后,会在被修改的文本节点上触发DOMSubtreeModified事件;

该事件支持冒泡;

<div id="mydiv">大师哥王唯</div>
<script>
window.addEventListener("load", function(event){
    var oDiv = document.getElementById("mydiv");
    document.addEventListener("DOMSubtreeModified", function(event){
        console.log(event.type);
        console.log(event.target);
    },false);
    document.addEventListener("DOMCharacterDataModified", function(event){
        console.log(event.type);
        console.log(event.target);
        console.log(event.prevValue);
        console.log(event.newValue);
    },false);
    oDiv.firstChild.nodeValue = "Web前端开发";
}, false);
</script>

MutationEvent在DOM Events 标准中已被删除,因为在它的设计中有缺陷,所以DOM最新事件标准提出使用Mutation Observers取代mutation事件;

避免用mutation事件的实际原因是性能问题和跨浏览器支持:

性能原因:

为DOM添加 mutation 监听器会降低修改DOM文档的性能(慢1.5-7倍),即使移除监听器也不会提升性能;

MutationEvent中的所有事件都被设计成无法取消;如:

document.addEventListener("DOMNodeInserted", function(event){
    var div = document.createElement("div");
    document.body.appendChild(div);
});

MutationEvent是同步的,每次DOM的修改都会被触发,严重降低浏览器的运行,如:

var i=0;
document.addEventListener("DOMNodeInserted", function(event){
    i++;
});
oDiv.appendChild(document.createTextNode("1"));
console.log(i);  // 1
oDiv.appendChild(document.createTextNode("2"));
console.log(i);  // 2
oDiv.appendChild(document.createTextNode("3"));
console.log(i);  // 3

兼容性问题:

跨浏览器兼容性很差,这些事件在不同的浏览器实现并不一致, 例如:IE9之前的版本不支持mutation 事件,而且在IE9版本中没有正确实现其中某些事件(例如:DOMNodeInserted);另外,WebKit不支持DOMAttrModified事件;Firefox(到 version 11)不支持DOMElementNameChanged和 DOMAttributeNameChanged事件;

MutationObserver变动观察器:

提供了监视对DOM树所做更改的能力;它是旧的Mutation Events功能的替代品,该功能是DOM4 Events规范的一部分;

构造函数:MutationObserver(callback),创建并返回一个新的MutationObserver对象,其会在指定的DOM发生变化时被调用;

参数callback,是一个回调函数,每当被指定的节点或子树以及配置项有DOM变动时会被调用;

回调函数语法:function(Array MutationRecord, observer){};

回调函数拥有两个参数:一个是描述所有被改动记录的MutationRecord对象数组,另一个是调用该函数的MutationObserver对象;

function callback(mutationsList, observer){
    console.log(mutationsList);  // MutationRecord
    console.log(observer);  // MutationObserver
}
var observer = new MutationObserver(callback);

创建了MutationObserver对象后,对DOM的观察不会立即启动,必须调用observer()方法;

observe(target [, options])方法:

配置了MutationObserver对象的回调方法以开始接收与给定选项匹配的DOM变化的通知;(也就是设置观察目标)参数target为DOM树中的一个要观察变化的DOM节点,或者是被观察的子节点树的根节点(就是观察目标);参数options为可选,是一个MutationObserverInit对象,此对象的配置项描述了DOM的哪些变化应该提供给当前观察者的callback;

var oDiv = document.getElementById("mydiv");
var config = {childList: true};
observer.observe(oDiv, config);
var h1 = document.createElement("h1");
h1.innerHTML = "Web前端开发";
oDiv.appendChild(h1);

MutaionObserverInit对象:其是一个字典对象,描述了MutationObserver的配置,主要被用作observe()方法的参数;配置如下:

childList:可选,设为true以监视目标节点添加或删除新的子节点时触发回调函数,反之不触发,默认值为 false;

observer.observe(oDiv, {
    childList: true
});
var h1 = document.createElement("h1");
h1.innerHTML = "Web前端开发";
oDiv.appendChild(h1);

但不是包括监视节点的子节点的添加或删除子节点操作;如:

oDiv.appendChild(document.createTextNode("HTML")); // 可以观察到
oDiv.childNodes[0].remove(); // 可以观察到
oDiv.childNodes[0].appendChild(document.createTextNode("SubTree")); // 观察不到

subtree :可选,设为true将监视范围扩展至目标节点整个节点树中的所有后代节点,而不仅仅只作用于目标节点,默认值为 false;

observer.observe(oDiv, {
    childList: true, subtree: true
});

subtree可以与其他选项一起使用,以将属性、文本内容和子列表的监视扩展到以目标节点为根的整个子树;

另外需要注意,MutationObserver的callback回调函数是异步的,只有在全部DOM操作完成之后才会调用callback,如:

var oDiv = document.getElementById("mydiv");
var i=0;
var observer = new MutationObserver(function(mutations, observer){
    console.log(mutations);
    console.log("mutaion i:" + (i++));
});
observer.observe(oDiv, {
    childList: true
});
oDiv.appendChild(document.createTextNode("HTML"));
oDiv.appendChild(document.createTextNode("CSS"));
oDiv.appendChild(document.createTextNode("Javascript"));
console.log("i=" + i);  // 先执行,并且i=0
setTimeout(function(){
    console.log("i=" + i);  // 延迟执行,i=1
},50);

可以看出MutationObserver的特点:

  • 它是异步触发的,即会等待当前所有 DOM 操作都结束才触发,这样设计是为了应对 DOM 频繁变动的问题;
  • 它把 DOM 变动记录封装成一个数组进行统一处理,而不是一条一条进行处理;
  • 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动;

attributes :可选,设为true以观察受监视元素的属性及属性值的变动,包括属性的添加与删除,默认值为false;

observer.observe(oDiv, {
    attributes: true,
    childList: true
});
oDiv.setAttribute("status","1");  // 可以观察到
oDiv.style.color = "red";  // 可以观察到
oDiv.removeAttribute("name");  // 可以观察到
oDiv.className = "mydiv";  // 可以观察到

attributeFilter:可选,为要监视的特定属性名称的数组,如果未包含此属性,则对所有属性的更改都会触发变动通知;

observer.observe(oDiv, {
    attributeFilter:["status","class"],
    childList: true
});
oDiv.setAttribute("status","1");  // 可以观察到
oDiv.style.color = "red";  // 观察不到
oDiv.removeAttribute("name");  // 观察不到
oDiv.className = "mydiv";  // 可以观察到

注:如果指定了该属性,则attributes选项不能设置为false,否则抛出TypeError异常;

attributeOldValue :可选,当监视节点的属性改动时,将此属性设为true将记录任何有改动的属性的上一个值,也就是是否应在MutationObserver.oldValue属性中包含已更改属性的先前值,如果为true,则相应地设置oldValue,否则oldValue为null;

示例:

function statusChange(username, status){}
function usernameChange(username, oldValue){}
function callback(mutationsList, observer){
    mutationsList.forEach(function(mutation){
        switch(mutation.type){
            case "attributes":
                switch(mutation.attributeName){
                    case "status":
                        statusChange(mutation.target.username, mutation.target.status);
                        break;
                    case "username":
                        usernameChange(mutation.target.username, mutation.oldValue);
                        break;
                }
                break;
        }
    });
}
var observer = new MutationObserver(callback);
var userList = document.getElementById("userlist");
observer.observe(userList, {
    attributeFilter: ["status", "username"],
    attributeOldValue: true,
    subtree: true
});

characterData:可选,用来观察CharacterData类型的节点变化的,设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化;字符数据的变化可以在任何文本节点上检测到,包括基于文本、处理指令和注释接口的节点;

注意,不会监视HTMLElement的内容,即使它只包含文本,因为它只监视文本节点本身,因此,要么直接将文本节点传递给observe()方法,要么还需要设置subtree:true;

var oDiv = document.getElementById("mydiv");
var observer = new MutationObserver(function(mutations, observer){
    console.log(mutations);
});
observer.observe(oDiv, {
    characterData: true, subtree: true
});
oDiv.appendChild(document.createTextNode("HTML")); // 观察不到
oDiv.childNodes[0].textContent = "Web前端开发";  // childNodes[0]指向的是oDiv中的空白符文本节点
oDiv.childNodes[1].textContent = "零点程序员"; // 观察不到,childNodes[1]是一个HtmlElement节点
oDiv.appendChild(document.createTextNode("大师哥王唯")); // 观察不到,这是添加子节点
oDiv.childNodes[0].remove();  // 删除文本节点也观察不到

characterDataOldValue:可选,设为true以在文本在受监视节点上发生更改时记录节点文本的先前值,并且characterData也会被自动设定为true,即使您没有明确地将其设置为true;

如果将characterData属性设置为true,但未将characterDataOldValue设置为true,则MutationRecord将不包含描述文本节点内容先前状态的信息;

注意:该配置对象中,childList、attributes 或者 characterData 三个选项之中,至少有一个必须为 true,否则会抛出 TypeError 异常;

MutaionObserverInit选项与MutationEvent之间的对应关系:

MutationEvent MutationObserver options
DOMNodeInserted { childList: true, subtree: true }
DOMNodeRemoved { childList: true, subtree: true }
DOMSubtreeModified { childList: true, subtree: true }
DOMAttrModified { attributes: true, subtree: true }
DOMCharacterDataModified{ characterData: true, subtree: true }

从这里可以看出,MutationObserver拥有非常大的灵活性;

示例,编辑一个元素的内容,并获取所变动的内容,如:

<ol contenteditable="true">
    <li>回车可以添加li</li>
</ol>
<script>
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var ol = document.querySelector("ol");
var observer = new MutationObserver(function(mutations){
    mutations.forEach(function(mutation){
        if(mutation.type === "childList"){
            var listValues = [].slice.call(ol.children)
                        .map(function(node){
                            return node.innerHTML;
                        })
                        .filter(function(s){
                            if(s === "<br>")
                                return false;
                            else
                                return true;
                        });
            console.log(listValues);
        }
    });
});
observer.observe(ol, {
    attributes: true,
    childList: true,
    characterData: true
});
</script>

MutationRecord对象:回调函数中的第一个参数是保存着MutationRecord对象的数组;

function callback(mutationsList, observer){
    console.log(mutationsList);  // [MutationRecord]
    console.log(mutationsList[0]);  // MutationRecord
}

每个MutationRecord都代表一个独立的DOM变化,在每次随DOM变化调用的回调函数时,一个相应的MutationRecord会被作为参数,传递给回调函数,并添加到MutationRecord数组中;

MutationRecord对象属性:

  • type:返回字符串,如果是属性变化,则返回 "attributes",如果是characterData节点变化,则返回 "characterData",如果是子节点树childList变化,则返回"childList";
  • target:根据type属性,返回变化所影响的节点;对于属性 attributes 变化,返回属性变化的节点,对于characterData变化,返回characterData节点,对于子节点树childList变化,返回子节点变化的节点;
  • addedNodes:返回被添加的节点NodeList;如果没有节点被添加,则该属性将是一个空的NodeList;
  • removedNodes:返回被移除的节点的NodeList;如果没有节点被移除,则是一个空的 NodeList;
  • previousSibling:返回被添加或移除的节点之前的兄弟节点,或者null;
  • nextSibling:返回被添加或移除的节点之后的兄弟节点,或者null;
  • attributeName:返回被修改的属性的属性名,或者null;
  • attributeNamespace:返回被修改属性的命名空间,或者null;
  • oldValue:返回值取决于type属性,对于属性attributes 变化,返回变化之前的属性值,对于 characterData变化,返回变化之前的数据,对于子节点树childList变化,返回null;注意,如果要让这个属性起作用,在相应的MutationObserverInit配置对象中,attributeOldValue 或 characterDataOldValue 必须设置为 true;
function callback(mutationsList, observer){
    console.log(mutationsList);
    var mutation = mutationsList[0];
    console.log(mutation.type);  // childList
    console.log(mutation.target);  // div
    console.log(mutation.addedNodes);  // NodeList[]
    console.log(mutation.removedNodes);  // NodeList[]
    console.log(mutation.previousSibling);
    console.log(mutation.nextSibling);
    console.log(mutation.attributeName);
    console.log(mutation.oldValue);
}
var observer = new MutationObserver(callback);
var oDiv = document.getElementById("mydiv");
var config = {
    attributes: true,
    attributeOldValue: true,
    childList: true
};
observer.observe(oDiv, config);
var h1 = document.createElement("h1");
h1.innerHTML = "Web前端开发";
oDiv.appendChild(h1);
oDiv.setAttribute("class","mydiv");
oDiv.setAttribute("class","yourdiv");
// 如
function callback(mutationsList, observer){
    mutationsList.forEach(function(mutation){
        switch(mutation.type){
            case "childList":
                // 添加或删除子节点了,可以获取
                console.log(mutation.addedNodes); // 或
                console.log(mutation.removedNodes); // 或者其他处理
                break;
            case "attributes":
                // 可以获取更改的属性名attributeName和之前的值oldValue
                console.log(mutation.attributeName + ":" + mutation.oldValue);
                break;
        }
    });
}

示例:统计还可以输入多少字,如:

<div id="editor" contenteditable="true" style="width:400px; height:200px; border:1px solid"></div>
<p id="textinputcount">还可以输入1000字</p>
<script>
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var editor = document.querySelector("#editor");
var textinputcount = document.querySelector("#textinputcount");
var observer = new MutationObserver(function(mutations){
    mutations.forEach(function(mutation){
        if(mutation.type === "characterData"){
            var newValue = mutation.target.textContent;
            textinputcount.innerHTML = "还可以输入" + (1000 - newValue.length) + "字";
        }
    });
});
observer.observe(editor, {
    attributes: true,
    childList: true,
    characterData: true,
    characterDataOldValue: true,
    subtree: true
});
</script>

示例:包装一个自定义的输出对象;

<div id="output">输出内容</div>
<div id="editor" contenteditable="true">随便什么内容</div>
<script>
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var editor = document.querySelector("#editor");
var output = document.querySelector("#output");
var observer = new MutationObserver(function(mutations){
    mutations.forEach(function(mutation){
        var entry = {
            mutation: mutation,
            target: mutation.target,
            value: mutation.target.textContent,
            oldValue: mutation.oldValue
        };
        console.log("entry:", entry);
        if(mutation.type === "characterData"){
            output.textContent = mutation.target.textContent;
        }
    });
});
observer.observe(editor, {
    attributes: true,
    childList: true,
    characterData: true,
    characterDataOldValue: true,
    subtree: true
});
</script>

复用 MutationObserver对象:

可以多次调用同一个MutationObserver对象的observe()方法,来观察DOM树中不同部分的变化,但需要注意两个问题:

如果在已经被同一MutationObserver观察的节点上调用observe()方法,则在激活新观察者之前,所有现有观察者将自动从所有正在观察的目标中移除;

如果同一个MutationObserver还没有作用在target上,则保留现有观察者并添加新观察者;

disconnect()方法:

告诉观察者停止观察变动,也就是阻止MutationObserver对象继续接收的通知;之后,还可以再次调用observe()方法来重用观察者;

function callback(mutationsList, observer){
    mutationsList.forEach(function(mutation){
        console.log(mutation.type);
    });
}
var observer = new MutationObserver(callback);
var oDiv = document.getElementById("mydiv");
observer.observe(oDiv, {
    childList: true,
    attributes: true
});
var h1 = document.createElement("h1");
h1.innerHTML = "Web前端开发";
oDiv.appendChild(h1);
// 延迟下,否则还没有添加子元素,就执行了
setTimeout(function(){
    observer.disconnect();
    oDiv.setAttribute("class","mydiv");  // 不会监视
    observer.observe(oDiv, {
        childList: true,
        subtree: true,
        attributes: true
    });
    oDiv.firstElementChild.style.color = "red"; // 监视了
},50);

如果被观察的元素被从DOM中移除,然后被浏览器的垃圾回收机制释放,此MutationObserver将同样被删除;

takeRecords()方法:

返回已检测到但尚未由观察者的回调函数处理的所有匹配DOM更改的列表,使变更记录队列保持为空;此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改;

该方法返回一个MutationRecord 对象数组;

注意: 调用takeRecords()后,已发生但未传递给回调的变更队列将被空;

// 在前面的代码上修改
oDiv.appendChild(document.createTextNode("Web前端开发"));
oDiv.setAttribute("status",1);  // 如果没有以下代码,记录列表中有两个
var records = observer.takeRecords();
console.log(records); // 此时records数组中保存了改变记录列表
// 当调用了takeRecords()时,记录队列被清空,因此不会触发MutationObser中的callback回调方法
// 但新的操作可以被观察到
oDiv.appendChild(document.createElement("h1"));  // 可以观察到
// 所有未处理的变更记录,然后调用回调,并将变更记录列表传递给回调,以保证所有变更记录都被处理;
// 这是在调用disconnect()之前完成的,以便停止观察DOM
if(records){
    callback(records);
}
observer.disconnect();  // 停止观察

异常:以下任一情况都会抛出异常:

  • 配置选项使得实际上不会监视任何内容,例如,如果childList、attributes和 characterData都为 false;
  • attributes选项为false,表示不监视属性更改,但是attributeOldValue为true,或attributeFilter配置存在;
  • characterDataOldValue选项为true,但是characterData为false(表示不跟踪字符更改);

可以取代DOMContentLoaded事件:

如:

function callback(mutations, observer){}
var observer = new MutationObserver(callback);
observer.observe(document.documentElement, {
    childList: true, subtree: true
});

使用MutationObserver对象封装一个监听DOM生成的函数,如:

(function(win){
    "use strict";
    var listeners = [];
    var doc = win.document;
    var MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
    var observer;
    function ready(selector, callback){
        // 存储选择器和回调函数
        listeners.push({
            selector: selector,
            callback: callback
        });
        if(!observer){
            // 监听document变化
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // 检查该节点是否已经在DOM中
        check();
    }
    function check(){
        // 检查是否匹配已存储的节点
        for(var i=0; i<listeners.length; i++){
            var listener = listeners[i];
            // 检查指定节点是否有匹配
            var elements = doc.querySelectorAll(listener.selector);
            for(var j=0, len=elements.length; j<len; j++){
                var element = elements[j];
                // 确保回调函数只会对该元素调用一次
                if(!element.ready){
                    element.ready = true;
                    // 对该节点调用回调函数
                    listener.callback.call(element, element);
                }
            }
        }
    }
    // 对外暴露ready
    win.ready = ready;
})(this);
ready(".container", function(element){
    console.log(element);
});
示例:实现undo和redo功能,保存为Mutation.js,如:
function Mutation(dom){
    var MutationObserver = this.MutationObserver ||
                            window.MutationObserver ||
                            window.WebKitMutationObserver ||
                            window.MozMutationObserver;
    // 判断浏览器是否支持MutationObserver
    this.mutationObserverSupport = !!MutationObserver;
    // 默认监听子元素、子元素的属性、属性值的改变
    this.options = {
        childList: true,
        subtree: true,
        attributes: true,
        characterData: true,
        attributeOldValue: true,
        characterDataOldValue: true
    };
    // 此属性保存了MutationObserver的实例
    this.muta = {};
    this.list = [];  // 保存了用户的操作
    this.index = 0;  // 当前回退的索引
    // 如果没有dom的话,就默认监听body
    this.dom = dom || document.body;
    // 开始监听
    this.observe();
}
// 节点发生改变的回调,要把redo和undo都保存在list中
Mutation.prototype.callback = function(records, instance){
    // 把索引后面的清除
    this.list.splice(this.index + 1);
    var _this = this;
    records.map(function(record){
        var target = record.target;
        console.log(record);
        // 删除元素或者是添加元素
        if(record.type === "childList"){
            // 如果是删除元素
            if(record.removedNodes.length !== 0){
                // 获取元素的相对索引
                var indexs = _this.getIndexs(target.children, record.removedNodes);
                _this.list.push({
                    "undo": function(){
                        _this.disconnect();
                        _this.addChildren(target, record.removedNodes, indexs);
                        _this.reObserve();
                    },
                    "redo": function(){
                        _this.disconnect();
                        _this.removeChildren(target, record.removedNodes);
                        _this.reObserve();
                    }
                });
            }
            // 如果是添加元素
            if(record.addedNodes.length !== 0){
                // 获取元素的相对索引
                var indexs = _this.getIndexs(target.children, record.addedNodes);
                _this.list.push({
                    "undo": function(){
                        _this.disconnect();
                        _this.removeChildren(target, record.addedNodes);
                        _this.reObserve();
                    },
                    "redo": function(){
                        _this.disconnect();
                        _this.addChildren(target, record.addedNodes, indexs);
                        _this.reObserve();
                    }
                });
            }
        }else if(record.type === "characterData"){
            var oldValue = record.oldValue;
            var newValue = record.target.textContent || record.target.innerText;
            _this.list.push({
                "undo": function(){
                    _this.disconnect();
                    target.textContent = oldValue;
                    _this.reObserve();
                },
                "redo": function(){
                    _this.disconnect();
                    target.textContent = newValue;
                    _this.reObserve();
                }
            });
        }else if(record.type === "attributes"){
            var oldValue = record.oldValue;
            var newValue = record.target.getAttribute(record.attributeName);
            var attributeName = record.attributeName;
            _this.list.push({
                "undo": function(){
                    _this.disconnect();
                    target.setAttribute(attributeName, oldValue);
                    _this.reObserve();
                },
                "redo": function(){
                    _this.disconnect();
                    target.setAttribute(attributeName, newValue);
                    _this.reObserve();
                }
            });
        }
    });
    // 重新设置索引
    this.index = this.list.length - 1;
};
Mutation.prototype.removeChildren = function(target, nodes){
    for(var i=0, len=nodes.length; i<len; i++){
        target.removeChild(nodes[i]);
    }
};
Mutation.prototype.addChildren = function(target, nodes, indexs){
    for(var i=0,len=nodes.length; i<len; i++){
        if(target.children[indexs[i]])
            target.insertBefore(nodes[i], target.children[indexs[i]]);
        else
            target.appendChild(nodes[i]);
    }
};
// 快捷方法,用来判断child在父元素的哪个节点上
Mutation.prototype.indexOf = function(target, obj){
    return Array.prototype.indexOf.call(target, obj);
};
Mutation.prototype.getIndexs = function(target, objs){
    var result = [];
    for(var i=0; i<objs.length; i++){
        result.push(this.indexOf(target, objs[i]));
    }
    return result;
};
Mutation.prototype.observe = function(){
    if(this.dom.nodeType !== 1)
        return alert("参数不对,第一个参数应该为一个dom节点");
    this.muta = new MutationObserver(this.callback.bind(this));
    // 开始监听
    this.muta.observe(this.dom, this.options);
};
// 重新开始监听
Mutation.prototype.reObserve = function(){
    this.muta.observe(this.dom, this.options);
};
// 不记录dom操作,在这个函数内部的操作不会记录到undo和redo的列表中
Mutation.prototype.without = function(callback){
    this.disconnect();
    callback & callback();
    this.reObserve();
};
// 取消监听
Mutation.prototype.disconnect = function(){
    return this.muta.disconnect();
};
// 保存Mutation操作到list
Mutation.prototype.save = function(obj){
    if(!obj.undo)
        return alert("传进来的第一个参数必须有undo方法");
    if(!obj.redo)
        return alert("传进来的第一个参数必须有redo方法");
    this.list.push(obj);
};
// 清空数组
Mutation.prototype.reset = function(){
    this.list = [];
    this.index = 0;
};
// 把指定index后面的操作删除
Mutation.prototype.splice = function(index){
    this.list.splice(index);
};
// 往回走,取消回退
Mutation.prototype.undo = function(){
    if(this.canUndo()){
        this.list[this.index].undo();
        this.index--;
    }
};
// 往前走,重新操作
Mutation.prototype.redo = function(){
    if(this.canRedo()){
        this.index++;
        this.list[this.index].redo();
    }
};
// 判断是否可以撤销操作
Mutation.prototype.canUndo = function(){
    return this.index !== -1;
}
// 判断是否可以重新操作
Mutation.prototype.canRedo = function(){
    return (this.list.length - 1) !== this.index;
}
应用:
<div style="padding: 20px; border:1px solid;">
    <input type="button" value="撤销操作" id="prev" />
    <input type="button" value="撤销操作回退" id="next" />
</div>
<p>
<input type="button" value="添加节点" id="add">
<input type="text" value="text" id="txt">
</p>
<div id="mydiv"></div>
<script>
window.onload = function(){
    var observer = new Mutation();
    observer.disconnect();  // 取消监听
    observer.reObserve();  // 重新监听
    document.getElementById("add").addEventListener("click", function(event){
        var div = document.createElement("div");
        div.innerHTML = document.getElementById("txt").value;
        document.getElementById("mydiv").appendChild(div);
    },false);
    document.getElementById("prev").addEventListener("click", function(event){
        observer.undo();
    },false);
    document.getElementById("next").addEventListener("click", function(event){
        observer.redo();
    },false);
};
</script>

Tags:

最近发表
标签列表