1. 程式人生 > >原生js實現檢測物件變化

原生js實現檢測物件變化

最近這段時間,前端開發開始逐漸模組化,一些MVC、MVVM等框架比較流行,比如angular、vue、react;這三個框架比較相似的有一點就是資料的雙向繫結,檢視的更新導致相應資料變化,資料的改變引起檢視的變化。像這種魔法的操作是怎麼實現的呢?像angular則是採用了‘髒值檢測的方式’資料發生變更後,對於所有的數據和檢視的繫結關係進行一次檢測,識別是否有資料發生了改變,有變化進行處理,可能進一步引發其他資料的改變,所以這個過程可能會迴圈幾次,一直到不再有資料變化發生後,將變更的資料傳送到檢視,更新頁面展現。如果是手動對 ViewModel 的資料進行變更,為確保變更同步到檢視,需要手動觸發一次“髒值檢測”。VueJS 則使用 ES5 提供的 Object.defineProperty() 方法,監控對資料的操作,從而可以自動觸發資料同步。並且,由於是在不同的資料上觸發同步,可以精確的將變更傳送給繫結的檢視,而不是對所有的資料都執行一次檢測。接下來就一塊實現檢測物件變化的功能。(本程式碼使用es6編寫,部分瀏覽器不支援,還需要使用babel進行編譯)

首先,js中的屬性分為倆種,一種是資料屬性,一種是訪問器屬性。

var data = {};
data.name = '田二黑';
上面這種就是資料屬性。當然和下面效果一樣:
Object.defineProperty(obj, 'name', {  
    value: '田二黑',       // 屬性的值  
    writable: true,     // 是否可寫  
    enumerable: true,   // 是否能夠通過for in 列舉  
    configurable: true  // 是否可使用 delete刪除  
})
當然我們可以定義訪問器屬性 get  set,當你讀取age屬性時,會自動呼叫get,設定屬性時會呼叫set
Object.defineProperty(obj, 'age', {  
    get: function(){  
        return 20;  
    },  
    set: function(newVal){  
        this.age += 20;  
    }  
})
其中,vue就是利用訪問器實現的資料雙向繫結,像下面這個例子(可能你家沒滿月的孩子都會寫了)
new Vue({  
   data:{  
  name:'田二黑',  
    age:21  
    }  
})
如果我們把data物件的屬性全部轉化為訪問器屬性,那我們不就可以檢測變化了,修改時候會呼叫set訪問器,在裡面回撥通知不就行了?
const OP = Object.prototype;  
const types = {  
  obj:'[object Object]',  
  array:'[object Array]'  
}  
export default class Jsonob{  
    constructor(obj,cb){  
        if(OP.toString.call(obj) !== types.obj){  
            console.log('請傳入一個物件');  
            return false;  
        }  
        this._callback = cb;  
        this.observe(obj);  
    }  
    observe(obj){  
        Object.keys(obj).forEach((key)=>{  
            let val = obj[key];  
            Object.defineProperty(obj,key,{  
                get:function(){  
                    return val;  
                },  
                set:(function(newVal){  
                    this._callback(newVal)  
                    val = newVal  
                }).bind(this)  
            })  
        },this)  
    }  
}  
上面程式碼聲明瞭類Jsonob,接收要監聽的物件和回撥函式;observe方法,遍歷該物件,並依次將物件屬性轉為訪問器屬性,在set中回撥通知。

接下來我們測試一下

import Jsonob from './jsonOb'  
var data = {  
    a: 200,  
    level1: {  
        b: 'str',  
        c: [1, 2, 3],  
        level2: {  
            d: 90  
        }  
    }  
}  
var cb = (val)=>{  
    console.log(val)  
}  
new Jsonob(data,cb);  
data.level1.level2.d = 50  
當修改物件data中屬性時,回撥打印出新的值。這樣還沒結束,我的舊值去哪了,我想獲取舊值咋辦?並且如果我設定的新值又是個物件咋辦
let val = obj[key];  
Object.defineProperty(obj,key,{  
get:function(){  
     return val;  
    },  
set:(function(newVal){  
     this._callback(newVal)  
     val = newVal  
    }).bind(this)  
})
上面的val = obj[key];儲存的不就是舊值嗎?於是修改程式碼如下
Object.keys(obj).forEach((key)=>{  
            let oldVal = obj[key];  
            Object.defineProperty(obj,key,{  
                get:function(){  
                    return oldVal;  
                },  
                set:(function(newVal){  
                    if(oldVal !== newVal){  
                        if(OP.toString.call(newVal) === '[object Object]'){  
                                this.observe(newVal);  
                            }  
                        this._callback(newVal,oldVal)  
                        oldVal = newVal  
                    }  
                }).bind(this)  
            })  
            if(OP.toString.call(obj[key]) === types.obj){  
                this.observe(obj[key])  
            }  
        },this)
判斷修改的值是否為物件,如果是物件,則繼續轉換新增的值的屬性為訪問器屬性。在回撥中就能接收新值和舊值。當然相信你已經發現了,

data.leavel.c是個陣列,當我們push,shift等操作時還監聽不到,首先,當我們呼叫陣列的push等方法時,是執行的陣列原型上的方法,那我們重

寫原型上的這些方法,在這些方法裡面監聽不就ok了,像這樣

Array.prototype.push = function(){  
    /********/  
Array.prototype.shift= function(){  
    /********/  
}

陣列有push,shift,pop,unshift等等,你要重寫那麼多方法並實現其功能,就算你實現了,並且不影響其他程式碼中陣列的使用,效能上來說也是不

相提並論的。那我們怎麼實現?我們可不可以讓陣列例項的原型指向一個我們自定義的物件fakeprototype,當我們呼叫push方法時,呼叫的是該

物件上的push方法,在方法裡面監聽變化,然後在呼叫Array.prototype真正原型物件上的push方法不就行了。程式碼實現如下:

const OP = Object.prototype;  
const types = {  
  obj:'[object Object]',  
  array:'[object Array]'  
}  
const OAM =['push','pop','shift','unshift','short','reverse','splice']  
export default class Jsonob{  
    constructor(obj,cb){  
        if(OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array){  
            console.log('請傳入一個物件或陣列');  
            return false;  
        }  
        this._callback = cb;  
        this.observe(obj);  
    }  
    observe(obj){  
        if(OP.toString.call(obj) === types.array){  
            this.overrideArrayProto(obj);  
        }  
        Object.keys(obj).forEach((key)=>{  
            let oldVal = obj[key];  
            Object.defineProperty(obj,key,{  
                get:function(){  
                    return oldVal;  
                },  
                set:(function(newVal){  
                    if(oldVal !== newVal){  
                        if(OP.toString.call(newVal) === '[object Object]'){  
                            this.observe(newVal);  
                        }  
                        this._callback(newVal,oldVal)  
                        oldVal = newVal  
                    }  
                }).bind(this)  
            })  
            if(OP.toString.call(obj[key]) === types.obj || OP.toString.call(obj[key]) === types.array){  
                this.observe(obj[key])  
            }  
        },this)  
    }  
    overrideArrayProto(array){  
            // 儲存原始 Array 原型  
        var originalProto = Array.prototype,  
            // 通過 Object.create 方法建立一個物件,該物件的原型是Array.prototype  
            overrideProto = Object.create(Array.prototype),  
            self = this,  
            result;  
            // 遍歷要重寫的陣列方法  
            OAM.forEach((method)=>{  
                Object.defineProperty(overrideProto,method,{  
                    value:function(){  
                        var oldVal = this.slice();  
                        //呼叫原始原型上的方法  
                        result = originalProto[method].apply(this,arguments);  
                        //繼續監聽新陣列  
                        // self.observe(this);  
                        self._callback(this,oldVal);  
                        return result;  
                    }  
                })  
            });  
        // 最後 讓該陣列例項的 __proto__ 屬性指向 假的原型 overrideProto  
        array.__proto__ = overrideProto;  
          
    }  
}
當我們再去對data.leave1.c.push()的時候,就能監聽到變化。然而還沒有完,我們現在只是知道了修改的新值和舊值,我們修改的哪個屬性啊?我們

在的程式還無法知道,像vue,在模板中<div>{{name}}</div><div>{{age}}</div> 如果name變化,只是修改第一個div,這就是知道修改哪個屬

性的好像,不然只能對模板重新全部重新整理,效能肯定是不如區域性修改的。因此我們還要在程式碼的基礎上加個路徑變數,表示是data的哪個屬性。

const OP = Object.prototype;  
const types = {  
  obj:'[object Object]',  
  array:'[object Array]'  
}  
const OAM =['push','pop','shift','unshift','short','reverse','splice']  
export default class Jsonob{  
    constructor(obj,cb){  
        if(OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array){  
            console.log('請傳入一個物件或陣列');  
            return false;  
        }  
        this._callback = cb;  
        this.observe(obj);  
    }  
    observe(obj,path){  
        if(OP.toString.call(obj) === types.array){  
            this.overrideArrayProto(obj,path);  
        }  
        Object.keys(obj).forEach((key)=>{  
            let oldVal = obj[key];  
            let pathArray = path&&path.slice();  
            if(pathArray){  
                pathArray.push(key);  
            }  
            else{  
                pathArray = [key];  
            }  
            Object.defineProperty(obj,key,{  
                get:function(){  
                    return oldVal;  
                },  
                set:(function(newVal){  
                    if(oldVal !== newVal){  
                        if(OP.toString.call(newVal) === '[object Object]'){  
                            this.observe(newVal,pathArray);  
                        }  
                        this._callback(newVal,oldVal,pathArray)  
                        oldVal = newVal  
                    }  
                }).bind(this)  
            })  
            if(OP.toString.call(obj[key]) === types.obj || OP.toString.call(obj[key]) === types.array){  
                this.observe(obj[key],pathArray)  
            }  
        },this)  
    }  
    overrideArrayProto(array,path){  
            // 儲存原始 Array 原型  
        var originalProto = Array.prototype,  
            // 通過 Object.create 方法建立一個物件,該物件的原型是Array.prototype  
            overrideProto = Object.create(Array.prototype),  
            self = this,  
            result;  
            // 遍歷要重寫的陣列方法  
            OAM.forEach((method)=>{  
                Object.defineProperty(overrideProto,method,{  
                    value:function(){  
                        var oldVal = this.slice();  
                        //呼叫原始原型上的方法  
                        result = originalProto[method].apply(this,arguments);  
                        //繼續監聽新陣列  
                        self.observe(this,path);  
                        self._callback(this,oldVal,path);  
                        return result;  
                    }  
                })  
            });  
        // 最後 讓該陣列例項的 __proto__ 屬性指向 假的原型 overrideProto  
        array.__proto__ = overrideProto;  
          
    }  

當第一次呼叫observe時,path為空,則pathArray將當前key傳入,如果不為空,則繼續追加path。好了,我們現在的程式算是比較完整了,知道

要修改的屬性,新值和就舊值。水平有限,希望指出共同進步。

相關推薦

原生js實現檢測物件變化

最近這段時間,前端開發開始逐漸模組化,一些MVC、MVVM等框架比較流行,比如angular、vue、react;這三個框架比較相似的有一點就是資料的雙向繫結,檢視的更新導致相應資料變化,資料的改變引起檢視的變化。像這種魔法的操作是怎麼實現的呢?像angular則是採用了‘

原生js實現拖動滑塊驗證

cnblogs tcc mvt wms 網站 hnu 按鈕 itl rip 拖動滑塊驗證是現在的網站隨處可見的,各式各樣的拖動法都有。 下面實現的是某寶的拖動滑塊驗證: <!DOCTYPE html> <html lang="en"> <he

原生js實現outerWidth()方法,用到getComputedStyle

turn left func ltview wid nts dst fault 方法 function getTrueStyle(obj,attr){ if(obj.currentStyle){ //ie return obj.currentStyle[at

原生js實現form表單序列化

defined json cnblogs 一個 break value 元素 default [0 大家都知道在jquery中有相應的表單序列化的方法: 1.serialize()方法   格式:var data = $("form").serialize();   功能:

h5原生js實現輪播圖

list sla head log startx creat ase hit eve 預覽地址:http://s.xnimg.cn/a90529/wap/wechatLive/test/slide.html <!DOCTYPE html> <html l

原生js實現數據單向綁定

web 名稱 target 接收參數 .org desc ctype html fin Object.defineProperty()方法直接在對象上定義一個新屬性,或修改對象上的現有屬性,並返回該對象。 Object.defineProperty(obj, prop, d

原生js實現清除子元素節點

spa class document cnblogs ech child mov log tor var table = document.body.querySelector(‘.mui-table-view‘);

原生JS實現tab切換--web前端開發

soft 楊冪 microsoft hidden isp 老婆 tex oct rip tab切換非常常見,應用非常廣泛,比較實用,一般來說是一個網站必不可少的一個效果。例如:https://123.sogou.com/中的一個tab部分: 1、案例源代碼 <!DO

原生js實現輪播

tex tom enter utf-8 定義 radi absolut tco query <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8">

原生js實現的瀑布流布局

n) top 開始 rrh lang java ansi return 一個 <!doctype html> <html lang="en"> <head> <meta charset="UTF-8">

原生JS實現增加刪除class

doctype [0 cls hasclass sna pla class ctype reg 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <style type="text/css

JQuery&原生js ——實現剪刀石頭布小遊戲

原生 type space 封裝 jquer 石頭 jquery java .com jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫( 或JavaScript框架)。jQuery設計的宗旨是&ldqu

原生JS實現放大鏡效果

use 瀏覽器 賦值 uri 字符串 () solid adding clas 效果: 1、 鼠標放上去會有半透明遮罩、右邊會有大圖片局部圖 2、 鼠標移動時右邊的大圖片也會局部移動 放大鏡的關鍵原理: 鼠標在小圖片上移動時,通過捕捉鼠標在小圖片上的位置,定位大圖片的相

原生js實現ajax、jsonp

原生js 斜杠 lang settime 發送數據 tro upper 類型 之前 一、原生js實現ajax $.ajax({ url: ‘‘, type: ‘post‘, data: {name: ‘zhaobao‘, age: 20}, dataTy

原生JS實現ajax省市區三聯

cal tex -type con arr length view thead () <!-- /** * @fileName: linkageUI.class.php * @author: wk * @DateTime: 2017/10/29 17:25 * @De

原生JS實現圖片放大鏡插件

spa ont 範圍 display 離開 寬度 部分 gin es2017 前 言   我們大家經常逛各種電商類的網站,商品的細節就需要用到放大鏡,這個大家一定不陌生,今天我們就做一個圖片放大鏡的插件,來看看圖片是如何被放大的……

原生js實現購物車相關功能

parse ole child number ble doctype oat gin button <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"

原生js實現三級聯動

三級聯動 chan 學習 initial ble ont view document chang 學習 自 於 http://blog.csdn.net/Elenal/article/details/51493510 <!DOCTYPE html> <ht

原生js實現簡單的焦點圖效果

begin pic false doctype 目標 16px urn 旅行 .cn 用到一些封裝好的運動函數,主要是定時器 效果為圖片和圖片的描述定時自動更換 <!DOCTYPE html> <html> <head>

原生js實現選項卡

index 定義 mar 事件綁定 click right get elements ext html代碼: <div class="tab"> <ul> <li class="selected">圖片</li&