文章出處

   MutationObserver介紹

   MutationObserver給開發者們提供了一種能在某個范圍內的DOM樹發生變化時作出適當反應的能力.該API設計用來替換掉在DOM3事件規范中引入的Mutation事件.

   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實現的undoredo(撤銷回退的管理)的管理插件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;

    }
});
View Code

   

  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的瀏覽器兼容性:

FeatureChromeFirefox (Gecko)Internet ExplorerOperaSafari
Basic support

18 webkit
26

14 (14) 11 15 6.0 WebKit

  阮一峰Mutationobserver

  MDN的資料:MutationObserver


作者:
NONO  

出處:http://www.cnblogs.com/diligenceday/
QQ:287101329 

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()