原生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,設定屬性時會呼叫setObject.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&