基于HTML5新特性Mutation Observer实现编辑器的撤销和回退操作


Posted in HTML / CSS onJanuary 11, 2016

MutationObserver介绍

MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.

Mutation Observer(变动观察器)是监视DOM变动的接口。当DOM对象树发生任何变动时,Mutation Observer会得到通知。

Mutation Observer有以下特点:

 •它等待所有脚本任务完成后,才会运行,即采用异步方式
 •它把DOM变动记录封装成一个数组进行处理,而不是一条条地个别处理DOM变动。
 •它即可以观察发生在DOM节点的所有变动,也可以观察某一类变动

MDN的资料: MutationObserver

MutationObserver是一个构造函数, 所以创建的时候要通过 new MutationObserver;

实例化MutationObserver的时候需要一个回调函数,该回调函数会在指定的DOM节点(目标节点)发生变化时被调用,

在调用时,观察者对象会 传给该函数 两个参数:

1:第一个参数是个包含了若干个MutationRecord对象的数组;

2:第二个参数则是这个观察者对象本身.

比如这样:

     

复制代码
代码如下:

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

observer的方法

实例observer有三个方法: 1: observe  ;2: disconnect ; 3: takeRecords   ;

observe方法

observe方法:给当前观察者对象注册需要观察的目标节点,在目标节点(还可以同时观察其后代节点)发生DOM变化时收到通知;

这个方法需要两个参数,第一个为目标节点, 第二个参数为需要监听变化的类型,是一个json对象,  实例如下:

       

复制代码
代码如下:

observer.observe( document.body, {
'childList': true, //该元素的子元素新增或者删除
'subtree': true, //该元素的所有子元素新增或者删除
'attributes' : true, //监听属性变化
'characterData' : true, // 监听text或者comment变化
'attributeOldValue' : true, //属性原始值
'characterDataOldValue' : true
});

disconnect方法

disconnect方法会停止观察目标节点的属性和节点变化, 直到下次重新调用observe方法;

takeRecords

清空 观察者对象的 记录队列,并返回一个数组, 数组中包含Mutation事件对象;

MutationObserver实现一个编辑器的redo和undo再适合不过了, 因为每次指定节点内部发生的任何改变都会被记录下来, 如果使用传统的keydown或者keyup实现会有一些弊端,比如:

1:失去滚动, 导致滚动位置不准确;

2:失去焦点;
....
用了几小时的时间,写了一个通过MutationObserver实现的 undo 和 redo (撤销回退的管理)的管理插件 MutationJS ,   可以作为一个单独的插件引入:(http://files.cnblogs.com/files/diligenceday/MutationJS.js):

复制代码
代码如下:

/**
* @desc MutationJs, 使用了DOM3的新事件 MutationObserve; 通过监听指定节点元素, 监听内部dom属性或者dom节点的更改, 并执行相应的回调;
* */
window.nono = window.nono || {};
/**
* @desc
* */
nono.MutationJs = function( 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
};
//这个保存了MutationObserve的实例;
this.muta = {};
//list这个变量保存了用户的操作;
this.list = [];
//当前回退的索引
this.index = 0;
//如果没有dom的话,就默认监听body;
this.dom = dom|| document.documentElement.body || document.getElementsByTagName("body")[0];
//马上开始监听;
this.observe( );
};
$.extend(nono.MutationJs.prototype, {
//节点发生改变的回调, 要把redo和undo都保存到list中;
"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();
}
});
};
//@desc characterData是什么鬼;
//ref : http://baike.baidu.com/link?url=Z3Xr2y7zIF50bjXDFpSlQ0PiaUPVZhQJO7SaMCJXWHxD6loRcf_TVx1vsG74WUSZ_0-7wq4_oq0Ci-8ghUAG8a
}else if( record.type === "characterData" ) {
var oldValue = record.oldValue;
var newValue = record.target.textContent //|| record.target.innerText, 不准备处理IE789的兼容,所以不用innerText了;
_this.list.push({
"undo" : function() {
_this.disconnect();
target.textContent = oldValue;
_this.reObserve();
},
"redo" : function () {
_this.disconnect();
target.textContent = newValue;
_this.reObserve();
}
});
//如果是属性变化的话style, dataset, attribute都是属于attributes发生改变, 可以统一处理;
}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;
},
"removeChildren" : function ( target, nodes ) {
for(var i= 0, len= nodes.length; i<len; i++ ) {
target.removeChild( nodes[i] );
};
},
"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在父元素的哪个节点上;
"indexOf" : function ( target, obj ) {
return Array.prototype.indexOf.call(target, obj)
},
"getIndexs" : function (target, objs) {
var result = [];
for(var i=0; i<objs.length; i++) {
result.push( this.indexOf(target, objs[i]) );
};
return result;
},
/**
* @desc 指定监听的对象
* */
"observe" : function( ) {
if( this.dom.nodeType !== 1) return alert("参数不对,第一个参数应该为一个dom节点");
this.muta = new this.MutationObserver( this.callback.bind(this) );
//马上开始监听;
this.muta.observe( this.dom, this.options );
},
/**
* @desc 重新开始监听;
* */
"reObserve" : function () {
this.muta.observe( this.dom, this.options );
},
/**
*@desc 不记录dom操作, 所有在这个函数内部的操作不会记录到undo和redo的列表中;
* */
"without" : function ( fn ) {
this.disconnect();
fn&fn();
this.reObserve();
},
/**
* @desc 取消监听;
* */
"disconnect" : function () {
return this.muta.disconnect();
},
/**
* @desc 保存Mutation操作到list;
* */
"save" : function ( obj ) {
if(!obj.undo)return alert("传进来的第一个参数必须有undo方法才行");
if(!obj.redo)return alert("传进来的第一个参数必须有redo方法才行");
this.list.push(obj);
},
/**
* @desc ;
* */
"reset" : function () {
//清空数组;
this.list = [];
this.index = 0;
},
/**
* @desc 把指定index后面的操作删除;
* */
"splice" : function ( index ) {
this.list.splice( index );
},
/**
* @desc 往回走, 取消回退
* */
"undo" : function () {
if( this.canUndo() ) {
this.list[this.index].undo();
this.index--;
};
},
/**
* @desc 往前走, 重新操作
* */
"redo" : function () {
if( this.canRedo() ) {
this.index++;
this.list[this.index].redo();
};
},
/**
* @desc 判断是否可以撤销操作
* */
"canUndo" : function () {
return this.index !== -1;
},
/**
* @desc 判断是否可以重新操作;
* */
"canRedo" : function () {
return this.list.length-1 !== this.index;
}
});

MutationJS如何使用

那么这个MutationJS如何使用呢?

复制代码
代码如下:

//这个是实例化一个MutationJS对象, 如果不传参数默认监听body元素的变动;
mu = new nono.MutationJs();
//可以传一个指定元素,比如这样;
mu = new nono.MutationJS( document.getElementById("div0") );
//那么所有该元素下的元素变动都会被插件记录下来;

Mutation的实例mu有几个方法:

1:mu.undo()  操作回退;

2:mu.redo()   撤销回退;

3:mu.canUndo() 是否可以操作回退, 返回值为true或者false;

4:mu.canRedo() 是否可以撤销回退, 返回值为true或者false;

5:mu.reset() 清空所有的undo列表, 释放空间;

6:mu.without() 传一个为函数的参数, 所有在该函数内部的dom操作, mu不做记录;

MutationJS实现了一个简易的 undoManager 提供参考,在火狐和chrome,谷歌浏览器,IE11上面运行完全正常:

复制代码
代码如下:

<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="http://cdn.bootcss.com/jquery/1.9.0/jquery.js"></script>
<script src="http://files.cnblogs.com/files/diligenceday/MutationJS.js"></script>
</head>
<body>
<div>
<p>
MutationObserver是为了替换掉原来Mutation Events的一系列事件, 浏览器会监听指定Element下所有元素的新增,删除,替换等;
</p>
<div style="padding:20px;border:1px solid #f00">
<input type="button" value="撤销操作" id="prev">;
<input type="button" value="撤销操作回退" id="next">;
</div>
<input type="button" value="添加节点" id="b0">;
<input value="text" id="value">
<div id="div"></div>
</div>
<script>
window.onload = function () {
window.mu = new nono.MutationJs();
//取消监听
mu.disconnect();
//重新监听
mu.reObserve();
document.getElementById("b0").addEventListener("click", function ( ev ) {
div = document.createElement("div");
div.innerHTML = document.getElementById("value").value;
document.getElementById("div").appendChild( div );
});
document.getElementById("prev").addEventListener("click", function ( ev ) {
mu.undo();
});
document.getElementById("next").addEventListener("click", function ( ev ) {
mu.redo();
});
};
</script>
</body>
</html>

DEMO在IE下的截图:

基于HTML5新特性Mutation Observer实现编辑器的撤销和回退操作

MutatoinObserver的浏览器兼容性:

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 18 webkit 26 14(14) 11 15 6.0WebKit
HTML / CSS 相关文章推荐
纯CSS3代码实现switch滑动开关按钮效果
Aug 30 HTML / CSS
浅析与CSS3的loading动画加载相关的transition优化
May 18 HTML / CSS
利用CSS3实现文字折纸效果实例代码
Jul 10 HTML / CSS
HTML5中的nav标签学习笔记
Jun 24 HTML / CSS
html5理解head_动力节点Java学院整理
Jul 13 HTML / CSS
突袭HTML5之Javascript API扩展4—拖拽(Drag/Drop)概述
Jan 31 HTML / CSS
HTML5 Canvas draw方法制作动画效果示例
Jul 11 HTML / CSS
HTML5 video 事件应用示例
Sep 11 HTML / CSS
Html5+JS实现手机摇一摇功能
Apr 24 HTML / CSS
HTML5 canvas基本绘图之文字渲染
Jun 27 HTML / CSS
小程序canvas中文字设置居中锚点
Apr 16 HTML / CSS
HTML5 直播疯狂点赞动画实现代码 附源码
Apr 14 HTML / CSS
Html5插件教程之添加浏览器放大镜效果的商品橱窗
Jan 07 #HTML / CSS
移动端html5 meta标签的神奇功效
Jan 06 #HTML / CSS
Web时代变迁及html5与html4的区别
Jan 06 #HTML / CSS
HTML5学习笔记之html5与传统html区别
Jan 06 #HTML / CSS
探讨HTML5移动开发的几大特性(必看)
Dec 30 #HTML / CSS
HTML5 history新特性pushState、replaceState及两者的区别
Dec 26 #HTML / CSS
实例讲解HTML5的meta标签的一些应用
Dec 08 #HTML / CSS
You might like
用PHP制作静态网站的模板框架(一)
2006/10/09 PHP
PHP htmlspecialchars() 函数实例代码及用法大全
2018/09/18 PHP
Javascript string 扩展库代码
2010/04/09 Javascript
nodejs 后缀名判断限制代码
2011/03/31 NodeJs
JavaScript高级程序设计 阅读笔记(四) ECMAScript中的类型转换
2012/02/27 Javascript
js实现身份证号码验证的简单实例
2014/02/19 Javascript
JavaScript如何实现在文本框(密码框)输入提示语
2015/12/25 Javascript
jQuery实现文档树效果
2017/02/20 Javascript
jquery ztree实现右键收藏功能
2017/11/20 jQuery
vue给组件传递不同的值方法
2018/09/29 Javascript
微信小程序实现搜索功能并跳转搜索结果页面
2019/05/18 Javascript
JQuery+Bootstrap 自定义全屏Loading插件的示例demo
2019/07/03 jQuery
Vue+Koa2 打包后进行线上部署的教程详解
2019/07/31 Javascript
js canvas实现星空连线背景特效
2019/11/01 Javascript
vue 实现路由跳转时更改页面title
2019/11/05 Javascript
Vue extend的基本用法(实例详解)
2019/12/09 Javascript
vue开发chrome插件,实现获取界面数据和保存到数据库功能
2020/12/01 Vue.js
[01:01:42]Secret vs Optic Supermajor 胜者组 BO3 第二场 6.4
2018/06/05 DOTA
python检查字符串是否是正确ISBN的方法
2015/07/11 Python
一波神奇的Python语句、函数与方法的使用技巧总结
2015/12/08 Python
python入门前的第一课 python怎样入门
2018/03/06 Python
Python编程图形库之Pillow使用方法讲解
2018/12/28 Python
Python 实现日志同时输出到屏幕和文件
2020/02/19 Python
Keras构建神经网络踩坑(解决model.predict预测值全为0.0的问题)
2020/07/07 Python
CSS3盒子模型详解
2013/04/24 HTML / CSS
阿里巴巴国际站:Alibaba.com
2016/07/21 全球购物
美国知名的女性服饰品牌:LOFT(洛芙特)
2016/08/05 全球购物
加拿大折扣、优惠券和交易网站:WagJag
2018/02/07 全球购物
婚前协议书怎么写
2014/04/15 职场文书
本科毕业生求职信
2014/06/15 职场文书
元旦趣味活动方案
2014/08/22 职场文书
2014年教师德育工作总结
2014/11/10 职场文书
档案管理员岗位职责
2015/02/12 职场文书
go语言中GOPATH GOROOT的作用和设置方式
2021/05/05 Golang
mysql事务隔离级别详情
2021/10/24 MySQL
Redis实战之Lettuce的使用技巧详解
2022/12/24 Redis