谈谈数据监听observable的实现
一、概述
数据监听实现上就是当数据变化时会通知我们的监听器去更新所有的订阅处理,如:
varvm=newObserver({a:{b:{x:1,y:2}}});
vm.watch(''a.b.x'',function(newVal,oldVal){
console.log(arguments);
});
vm.a.b.x=11;//触发watcher执行输出111
数据监听是对观察者模式的实现,也是MVVM中的核心功能。这个功能我们在很多场景中都可以用到,可以大大的简化我们的代码。
二、现有MVVM框架中的Observable是怎么实现的
先看看各MVVM框架对Observable是怎么实现的,我们分析下它们的实现原理,常见的MVVM框架有以下几种:1、knockout,老牌的MVVM实现
Firstname:
Lastname:
Hello,!
varViewModel=function(first,last){
this.firstName=ko.observable(first);
this.lastName=ko.observable(last);
this.fullName=ko.pureComputed(function(){
returnthis.firstName()+""+this.lastName();
},this);
};
ko.applyBindings(newViewModel("Planet","Earth"));
早期微软是把每个属性转换成一个observable函数,通过函数对该属性进行取值赋值来实现的,缺点是改变了原属性,不能够像属性一样取值赋值。
2、avalon,国产框架特点是兼容IE6+
{{w}}x{{h}}
W:
H:
varvm=avalon.define({
$id:"box",
w:100,
h:100,
click:function(){
vm.w=parseFloat(vm.w)+10;
vm.h=parseFloat(vm.h)+10;
}
});
avalon.scan()
avalon对数据监听堪称司徒的黑魔法,IE9+时利用ES5的defineProperty/defineProperties去实现,当IE不支持此方法时利用vbscript来实现。缺点是vbs定义后的对象不能够动态增删属性。
3、angular,大而全的mvvm解决方案
名:
姓:
姓名:{{firstName+""+lastName}}
varapp=angular.module(''myApp'',[]);
app.controller(''myCtrl'',function($scope){
$scope.firstName="John";
$scope.lastName="Doe";
});
ng对数据监听的实现,采用了AOP的编程思维,它对常用的dom事件xhr事件等进行封装,当这些事件被触发发,封装的方法中有去调用ng的digest流程,在此流程去检测数据变化并通知所有订阅,所以我们导致使用原生的setTimeout代替$timeout后需要自已去执行执行$digest()或$apply(),缺点是需要对使用到的所有外部事件进行封装。
4、vue,现代小巧优雅(实际上是比avalon大一些)
{{message}}
vardemo=newVue({
el:''#demo'',
data:{
message:''HelloVue.js!''
}
})
vue对数据监听的实现就比较单一了,因为它只支持IE9+,利用Object.defineProperty一招搞定。缺点是不兼容低版本IE。
三、Observable的实现有哪些方法及思路
通过上面几个框架对比我们可以看出几种不同数据监听的实现方法,实际上还有很多的方式可以去实现的:1、把属性转换为函数(knockout)2、IE9+使用defineProperty/defineProperties(vue、avalon)3、低版本IE使用VBS(avalon)4、数据检测,对各事件进行封装,在封装的方法中调用digest(angular)5、利用__defineGetter__/__defineSetter__方法(avalon)6、把数据转换成dom对象利用IE8dom对象的defineProperty方法或onpropertychange事件7、利用Object.observe方法8、利用ES6的Proxy对象9、利用setInterval进行脏检测
那么我们就具体看下这些数据监听实现:1、利用函数转换如ko.observable(),兼容所有
functionobservable(val){
returnfunction(newVal){
if(arguments.length>0){
val=newVal;
notifyChanges();
}else{
returnval;
}
}
}
vardata={};
vardata.a=observable(1);
varvalue=data.a()//取值
data.a(2);//赋值
2、利用defineProperty/defineProperties,兼容性IE9+
functiondefineReactive(obj,key,val){
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get:functionreactiveGetter(){
returnval;
},
set:functionreactiveSetter(newVal){
val=newVal;
notifyChanges();
}
});
}
3、利用__defineGetter__/__defineSetter__,兼容性一些mozilla内核的浏览器https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineGetter__
functiondefineReactive(obj,key,val){
obj.__defineGetter__(key,function(){
returnval;
});
obj.__defineSetter__(key,function(newVal){
val=newVal;
notifyChanges();
});
}
4、利用vbs,兼容性低版本的IE浏览器,IE11edge不再支持(avalon)先window.execScript得到parseVB的方法
FunctionparseVB(code)
ExecuteGlobal(code)
EndFunction
window.execScript(parseVB_Code);
然后处理好数据属性properties生成get/set方法放在accessors,并把notifyChanges放到get/set中,然后动态生成以下vbs代码
ClassDefinePropertyClass
Private[__data__],[__proxy__]
PublicDefaultFunction[__const__](d1,p1)
Set[__data__]=d1:set[__proxy__]=p1
Set[__const__]=Me
EndFunction
PublicPropertyLet[bbb](val1)
Call[__proxy__](Me,[__data__],"bbb",val1)
EndProperty
PublicPropertySet[bbb](val1)
Call[__proxy__](Me,[__data__],"bbb",val1)
EndProperty
PublicPropertyGet[bbb]
OnErrorResumeNext
Set[bbb]=[__proxy__](Me,[__data__],"bbb")
IfErr.Number<>0Then
[bbb]=[__proxy__](Me,[__data__],"bbb")
EndIf
OnErrorGoto0
EndProperty
PublicPropertyLet[ccc](val1)
Call[__proxy__](Me,[__data__],"ccc",val1)
EndProperty
PublicPropertySet[ccc](val1)
Call[__proxy__](Me,[__data__],"ccc",val1)
EndProperty
PublicPropertyGet[ccc]
OnErrorResumeNext
Set[ccc]=[__proxy__](Me,[__data__],"ccc")
IfErr.Number<>0Then
[ccc]=[__proxy__](Me,[__data__],"ccc")
EndIf
OnErrorGoto0
EndProperty
PublicPropertyLet[$model](val1)
Call[__proxy__](Me,[__data__],"$model",val1)
EndProperty
PublicPropertySet[$model](val1)
Call[__proxy__](Me,[__data__],"$model",val1)
EndProperty
PublicPropertyGet[$model]
OnErrorResumeNext
Set[$model]=[__proxy__](Me,[__data__],"$model")
IfErr.Number<>0Then
[$model]=[__proxy__](Me,[__data__],"$model")
EndIf
OnErrorGoto0
EndProperty
Public[$id]
Public[$render]
Public[$track]
Public[$element]
Public[$watch]
Public[$fire]
Public[$events]
Public[$skipArray]
Public[$accessors]
Public[$hashcode]
Public[$run]
Public[$wait]
Public[hasOwnProperty]
EndClass
FunctionDefinePropertyClassFactory(a,b)
Dimo
Seto=(NewDefinePropertyClass)(a,b)
SetDefinePropertyClassFactory=o;
EndFunction
执行以上两段vbs代码得到observable对象
window.parseVB(DefinePropertyClass_code);
window.parseVB(DefinePropertyClassFactory_code);
varvm=window.DefinePropertyClassFactory(accessors,VBMediator);
functionVBMediator(instance,accessors,name,value){
varaccessor=accessors[name]
if(arguments.length===4){
accessor.set.call(instance,value)
}else{
returnaccessor.get.call(instance)
}
}
5、在事件中触发检测digest,兼容所有(angular)以发XMLHttpRequest为例
var_XMLHttpRequest=window.XMLHttpRequest;
window.XMLHttpRequest=function(flags){
varreq;
req=new_XMLHttpRequest(flags);
monitorXHR(req);//处理req绑定触发数据检测及notifyChanges处理
returnreq;
};
6、把数据转换成dom节点再利用defineProperty方法或onpropertychange事件,这种极端的办法主要是用来处理IE8的,因为IE8支持defineProperty但只有DOM元素才支持
functiondata2dom(obj,key,val){
if(!objinstanceofHTMLElement){
obj=document.createElement(''i'');
}
//definePropertyoronpropertychangehandle
defineProperty(obj,key,val);//内部处理notifyChanges
returnobj;
}
这种方法的成本开销是很大的
7、利用Object.observe,在Chrome36beta版本中出现,但很多浏览器还没有支持已从ES7草案中移除
vardata={};
Object.observe(data,function(changes){
changes.forEach(function(change){
console.log(change.type,change.name,change.oldValue);
});
});
8、利用ES6的Proxy对象,未来的解决方案https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Global_Objects/Proxy
//语法
varp=newProxy(target,handler);
//示例
letsetter={
set:function(obj,prop,value){
obj[prop]=value;
notifyChanges();
}
};
letperson=newProxy({},setter);
person.age=28;//触发notifyChanges
9、利用脏检测,兼容所有,主要用于没有很好办法的情况下利用脏检测实现Object.defineProperty方法
functionPropertyChecker(obj,key,val,desc){
this.key=key;
this.val=val;
this.get=function(){
varval=desc.get();
if(this.val==val){
val=obj[key];
if(this.val!=val){
desc.set(val);
}
}
returnval;
};
this.set=desc.set;
}
varcheckList=[];
Object.defineProperty=function(obj,key,desc){
varval=obj[key]=desc.value!=undefined?desc.value:desc.get();
if(desc.get&&desc.set){
varproperty=newPropertyChecker(obj,key,val,desc);
checkList.push(property);
}
};
functionloopIE8(){
for(vari=0;i varitem=checkList[i];
varval=item.get();
if(item.val!=val){
item.val=val;
item.set(val);
}
}
}
setTimeout(function(){
setInterval(loopIE8,200);
},1000);
四、监听数组变化
实际上以面说的这些仅仅是对数据对象进行监听,而数据中还包括数组,如:
vardata={a:[1,2,3]};
data.a.push(4);
这种操作也会使数据产生了变化,但是仅对gettersetter进行定义是捕捉不到这些变化的。所以我们要单独针对数组做一些observable的处理。
基本思路就是重写数组的这些方法1、push2、pop,3、shift4、?unshift5、splice6、sort7、reverse
vararrayProto=Array.prototype;
vararrayMethods=Object.create(arrayProto);
vararrayKeys=Object.keys(arrayMethods);
[''push'',''pop'',''shift'',''unshift'',''splice'',''sort'',''reverse''].forEach(function(method){
varoriginal=arrayProto[method];
def(arrayMethods,method,functionmutator(www.sm136.com){
vari=arguments.length;
varargs=newArray(i);
while(i--){
args[i]=arguments[i];
}
varresult=original.apply(this,args);
varinserted;
switch(method){
case''push'':
inserted=args;
break;
case''unshift'':
inserted=args;
break;
case''splice'':
inserted=args.slice(2);
break;
}
if(inserted)observe(inserted);
notifyChanges();//通知变化
returnresult;
});
});
functiondef(obj,key,val,enumerable){
obj=Object.defineProperty(obj,key,{
value:val,
enumerable:!!enumerable,
writable:true,
configurable:true
})
}
functionprotoAugment(target,src){
target.__proto__=src;
}
functioncopyAugment(target,src,keys){
for(vari=0,l=keys.length;i varkey=keys[i];
def(target,key,src[key]);
}
}
var_augmentArr=(''__proto__''in{www.visa158.com})?protoAugment:copyAugment;
functionaugmentArr(arr){
_augmentArr(arr,arrayMethods,arrayKeys);
};
使用时只需要调用augmentArr(arr)即可实现
五、数据监听存在哪些问题
目前主流的数据监听方案还是defineProperty+augmentArr的方式,已有不少的mvvm框架及一些observable类库,但是还存在一些问题:1、所有的属性必须预先定义好
vardata=newObserver({a:{b:1}});//这里没有定义a.c
data.$watch(''a.c'',function(newVal,oldVal){
console.log(arguments);
});
data.a.c=1;//此时,监听a.c的watcher是不生效的,因为没有提前定义c属性
2、属性被覆盖后监听失效
vardata=newObserver({a:{b:1}});
data.$watch(''a.b'',function(newVal,oldVal){
console.log(arguments);
});
data.a.b=2;//生效
data.a={b:3};//此时b属性的原结构遭破坏,对b的监听失效
3、对数组元素的赋值是不会触发监听器更新的
vardata=newObserver({a{c:[1,2,3]}});
data.$watch(''a.c'',function(newVal,oldVal){
console.log(arguments);
});
data.a.c[1]=22;//不会触发a.c的watcher
这个问题,不少框架中是提供了一个$set方法来赋值,这是个解决问题的办法,但是原生代码赋值仍是不生效的。
def(arrayProto,''$set'',function$set(index,val){
if(index>=this.length){
this.length=Number(index)+1;
}
returnthis.splice(index,1,val)[0];
});
4、删除对象的属性也不会触发监听器更新
vardata=newObserver({a:{b:1},c:''xyz''});
data.$watch(''a'',function(newVal,oldVal){
console.log(arguments);
});
deletedata.a;//不会触发a的watcher
同数组也可以父节点中定义一个$remove来实现
六、这些问题的解决方案
上述问题中:1、第1、2其实是属于同一类的问题,就是因为这些notifyChanges直接在defineProperty时定义在属性中,当这个属性未定义或遭破坏时,那么对该属性的监听肯定是要失效的。对于这个问题的解决,我的思路是这样的
functionObserver(data){
this.data=data;
varwatches=[];
//监听时,先把监听数据保存在该observer实例的watches中
this.watch=function(path,subscriber,options){
watches.push(newWatcher(path,subscriber,options));
};
//当publish时把watcher转换为subscriber绑定到对应的属性上
this.publish=function(watch){
vartarget=queryProperty(watch.path);
varsubscriber=newSubscriber(watch,target);
target.ob.subscribes.add(subscriber);
}
}
每当重新赋新值时,会从根节点拉取watches重新publish,这样的话保证了赋新值时原来的监听数据不会被覆盖。
varob=newObserver(data);
ob.watch(''a.b'',function(){
console.log(arguments);
});
此watcher信息是保存在根节点的ob对象中,每一个object类型的属性都会对应一个ob对象,这样即使data.a={b:123}重新赋值导致data.a.b的定义被覆盖,但是根节点并没有被覆盖,在它被得新赋值时我们可以重新调用父节点ob中的publish方法把watcher重新生效,这样的话这个问题就可以解决了。
2、第3个问题,其实很容易解决,比如vue中只需要修改一句代码就可以解决,也许是出于性能还其它的考虑它没有这么去做。即把数组的每个元素当做属性来定义
functionobserveArr(arr){
for(vari=0,l=arr.length;i observeProperty(arr,i,arr[i]);
}
}
3、第4个问题除了父节点中增加$remove方法我目前也没有想到什么好的办法,如果大家有什么好的想法可以跟我交流下。
七、我对数据监听的实现
既然研究了下这个领域的东西,也就顺便造了个轮子实现了一个数据observable的功能,用法大概如下:
vardata={a:{b:{x:1,y:2}},c:[1,2,3]};
varob=newObserver(data);
data.$watch(''a.b'',function(){
console.log(arguments);
},{deep:true})
data.a.b.x=11;
|
|