js對象監聽實現

前言

隨着前端交互複雜度的提高,各種框架如angular,react,vue等也層出不窮,這些框架一個比較重要的技術點就是數據綁定。數據的監聽有較多的實現方案,本文將粗略的描述一番,並對其中一個兼容性較好的深刻分析。javascript

實現方案簡介

目前對象的監聽可行的方案:html

  • 髒檢查: 須要遍歷scope對象樹裏的$watch數組,使用不當容易形成性能問題前端

  • ES5 object.defineproperty: 除ie8部分支持 其餘基本都徹底支持vue

  • ES7 object.observe : 已經移除(原因)出ES7草案java

  • gecko object.watch :目前只有基於gecko的瀏覽器如火狐支持,官方建議僅供調試用react

  • ES6 Proxy: 目前支持較差,babel也暫不支持轉化git

ES5現代瀏覽器基本都支持了,OK,本文將介紹目前支持度最好的object.defineproperty 的Setters 和 Getters方式github

object.defineproperty介紹

簡潔的介紹

它屬於es5規範,有兩種定義屬性:數組

  • 一種是 數據屬性 包含Writable,Enumerable,Configurable瀏覽器

  • 一種是 訪問器屬性 包含get 和set

數據屬性的例子

obj.key='static';
//等效於
Object.defineProperty(obj, "key", {
  enumerable: true,
  configurable: true,
  writable: true,
  value: "static"
});

訪問器屬性例子

var obj = {
    temperature:'test'
};
var temperature='';
Object.defineProperty(obj, 'temperature', {
    get: function() {
        return temperature+'-----after';
    },
    set: function(value) {
        temperature = value;
    }
})
obj.temperature='Test';
//Test-----after
console.log(obj.temperature);

詳細的介紹

火狐開發者

實現監聽的思路

  1. 將須要監聽對象/數組 obj和回調函數callback傳入構造函數,this.callback = callback 存儲回調函數

  2. 遍歷對象/數組obj,經過Object.defineProperty將屬性所有定義一遍。在set函數裏面添加callback函數,設置val值。get函數返回val。

  3. 判斷對應的obj[key]是否爲對象,是則進入第二步,不然繼續遍歷

  4. 遍歷結束以後判斷該對象是否爲數組,是則對操做數組函數如push,pop,shift,unshift等進行封裝,操做數組前調用callback函數

數組的封裝

比較複雜的是數組的封裝,結構以下:
新建一個對象newProto,繼承Array的原型,並在newProto上面封裝push,pop等數組操做方法,再將傳入的array對象的原型設置爲newProto。

對應圖

圖片描述

路徑的定位

在獲取數據變化的同時,定位該變化數據在原始根對象的位置,以數組表示如:
如[ 'a', 'dd', 'ddd' ] 表示對象obj.a.dd.ddd的屬性改變
實現:每一個遍歷對象屬性都經過path.slice(0)的方式複製入參數組path,生成新數組tpath,給tpath數組push對應的對象屬性key,最後在執行set的回調函數時候將tpath當參數傳入

帶註釋代碼

watch.js

/**
 *
 * @param obj 須要監聽的對象或數組
 * @param callback 當對應屬性變化的時候觸發的回調函數
 * @constructor
 */
function Watch(obj, callback) {
    this.callback = callback;
    //監聽_obj對象 判斷是否爲對象,若是是數組,則對數組對應的原型進行封裝
    //path表明相應屬性在原始對象的位置,以數組表示. 如[ 'a', 'dd', 'ddd' ] 表示對象obj.a.dd.ddd的屬性改變
    this.observe = function (_obj, path) {
        var type=Object.prototype.toString.call(_obj);
        if (type== '[object Object]'||type== '[object Array]') {
            this.observeObj(_obj, path);
            if (type == '[object Array]') {
                this.cloneArray(_obj, path);
            }
        }
    };

    //遍歷對象obj,設置set,get屬性,set屬性能觸發callback函數,並將val的值改成newVal
    //遍歷結束後再次調用observe函數 判斷val是否爲對象,若是是則在對val進行遍歷設置set,get
    this.observeObj = function (obj, path) {
        var t = this;
        Object.keys(obj).forEach(function (prop) {
            var val = obj[prop];
            var tpath = path.slice(0);
            tpath.push(prop);
            Object.defineProperty(obj, prop, {
                get: function () {
                    return val;
                },
                set: function (newVal) {
                    t.callback(tpath, newVal, val);
                    val = newVal;
                }
            });
            t.observe(val, tpath);
        });
    };

    //經過對特定數組的原型中間放一個newProto原型,該原型繼承於Array的原型,可是對push,pop等數組操做屬性進行封裝
    this.cloneArray = function (a_array, path) {
        var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
        var arrayProto = Array.prototype;
        var newProto = Object.create(arrayProto);
        var t = this;
        ORP.forEach(function (prop) {
            Object.defineProperty(newProto, prop, {
                value: function (newVal) {
                    path.push(prop);
                    t.callback(path, newVal);
                    arrayProto[prop].apply(a_array, arguments);
                },
                enumerable: false,
                configurable: true,
                writable: true
            });
        });
        a_array.__proto__ = newProto;
    };

    //開始監聽obj對象,初始path爲[]
    this.observe(obj, []);
}

index.html

<body>
<ul>
    <li>
        <a href="javascript:void(0)" onClick="dataOne()">
            將obj b屬性改變
        </a>
    </li>
    <li>
        <a href="javascript:void(0)" onClick="dataTwo()">
            將obj a屬性的dd屬性的ddd屬性改變
        </a>
    </li>
    <li>
        <a href="javascript:void(0)" onClick="dataThree()">
            將obj a屬性的g屬性數組第一個值的a屬性改變
        </a>
    </li>
    <li>
        <a href="javascript:void(0)" onClick="dataFour()">
            將obj a屬性的g屬性數組push新的值
        </a>
    </li>
</ul>

<div id="path">
</div>
<div id="old-val">
</div>
<div id="new-val">
</div>
</body>
<script src="../src/watch.js"></script>
<script>
    var obj = {
        a: {e: 4, f: 5, g: [{a: 1, b: 2}, [3, 4]], dd: {ddd: 1}},
        b: 2,
        c: 3
    };

    new Watch(obj, call);
    function call(path, newVal, oldVal) {
        document.getElementById('path').innerHTML='路徑:'+path;
        document.getElementById('old-val').innerHTML='新的值:'+newVal;
        document.getElementById('new-val').innerHTML='老的值:'+oldVal;
    }

    function dataOne() {
        obj.b = Math.floor(Math.random()*10);
    }

    function dataTwo() {
        obj.a.dd.ddd = Math.floor(Math.random()*10);
    }

    function dataThree() {
        obj.a.g[0].a=Math.floor(Math.random()*10);
    }

    function dataFour() {
        obj.a.g.push(Math.floor(Math.random()*10));
    }
</script>

效果圖

圖片描述

代碼地址

完整代碼地址

流程圖

具體流程的複雜度基於監聽對象的深度,因此下圖只對父對象作流程分析
圖片描述

概括

  • 經過定義對象內部屬性的setter和getter方法,對將要變化的屬性進行攔截代理,在變化前執行預設的回調函數來達到對象監聽的目的。

  • 數組則在對象監聽以外額外在數組對象上的原型鏈上加一層原型對象,來攔截掉push,pop等方法,而後在執行預設的回調函數

最後

本文有什麼不完善的地方,或者流程圖有待改進的地方,敬請斧正。

相關文章
相關標籤/搜索