• 售前

  • 售后

热门帖子
入门百科

基于HTML5新特性Mutation Observer实现编辑器的打消和回退操纵

[复制链接]
我不是黄蓉500 显示全部楼层 发表于 2021-10-26 13:57:04 |阅读模式 打印 上一主题 下一主题
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 );
        };
    },
    "addChildren" : function ( target, nodes ,indexs) {
        for(var i= 0, len= nodes.length; i<len; i++ ) {
            if(target.children[ indexs ]) {
                target.insertBefore( nodes , target.children[ indexs ])  ;
            }else{
                target.appendChild( nodes );
            };
        };
    },
    //快捷方法,用来判断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) );
        };
        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下的截图:

MutatoinObserver的浏览器兼容性:
            Feature            Chrome            Firefox (Gecko)            Internet Explorer            Opera            Safari                            Basic support                        18            webkit
            26
                                    14(14)            11            15            6.0WebKit        

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作