谈谈数据监听observable的实现

时间:2022-08-28 14:48:35

一、概述

数据监听实现上就是当数据变化时会通知我们的监听器去更新所有的订阅处理,如:

var vm = new Observer({a:{b:{x:1,y:2}}});
vm.watch('a.b.x',function(newVal,oldVal){
console.log(arguments);
});
vm.a.b.x = 11; //触发watcher执行 输出 11 1

数据监听是对观察者模式的实现,也是MVVM中的核心功能。这个功能我们在很多场景中都可以用到,可以大大的简化我们的代码。

二、现有MVVM框架中的Observable是怎么实现的

先看看各MVVM框架对Observable是怎么实现的,我们分析下它们的实现原理,常见的MVVM框架有以下几种:

1、knockout,老牌的MVVM实现

<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
var ViewModel = function(first, last) {
this.firstName = ko.observable(first);
this.lastName = ko.observable(last); this.fullName = ko.pureComputed(function() {
return this.firstName() + " " + this.lastName();
}, this);
}; ko.applyBindings(new ViewModel("Planet", "Earth"));

早期微软是把每个属性转换成一个observable函数,通过函数对该属性进行取值赋值来实现的,缺点是改变了原属性,不能够像属性一样取值赋值。

2、avalon,国产框架特点是兼容IE6+

<div ms-controller="box">
<div style=" background: #a9ea00;" ms-css-width="w" ms-css-height="h" ms-click="click"></div>
<p>{{ w }} x {{ h }}</p>
<p>W: <input type="text" ms-duplex="w" data-duplex-event="change"/></p>
<p>H: <input type="text" ms-duplex="h" /></p>
</div>
var vm = 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解决方案

<div ng-app="myApp" ng-controller="myCtrl">
名: <input type="text" ng-model="firstName"><br>
姓: <input type="text" ng-model="lastName"><br>
<br>
姓名: {{firstName + " " + lastName}}
</div>
var app = 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大一些)

<div id="demo">
<p>{{message}}</p>
<input v-model="message">
</div>
var demo = new Vue({
el: '#demo',
data: {
message: 'Hello Vue.js!'
}
})

vue对数据监听的实现就比较单一了,因为它只支持IE9+,利用Object.defineProperty一招搞定。缺点是不兼容低版本IE。

三、Observable的实现有哪些方法及思路

通过上面几个框架对比我们可以看出几种不同数据监听的实现方法,实际上还有很多的方式可以去实现的:

1、把属性转换为函数(knockout

2、IE9+使用defineProperty/definePropertiesvueavalon

3、低版本IE使用VBS(avalon

4、数据检测,对各事件进行封装,在封装的方法中调用digest(angular

5、利用__defineGetter__/__defineSetter__方法(avalon

6、把数据转换成dom对象利用IE8 dom对象的defineProperty方法或onpropertychange事件

7、利用Object.observe方法

8、利用ES6的Proxy对象

9、利用setInterval进行脏检测

那么我们就具体看下这些数据监听实现:

1、利用函数转换如ko.observable(),兼容所有

function observable(val){
return function(newVal){
if (arguments.length > 0){
val = newVal;
notifyChanges();
}else{
return val;
}
}
}
var data = {};
var data.a = observable(1);
var value = data.a() //取值
data.a(2); //赋值

2、利用defineProperty/defineProperties,兼容性IE9+

function defineReactive(obj, key, val){
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val;
},
set: function reactiveSetter(newVal) {
val = newVal;
notifyChanges();
}
});
}

3、利用__defineGetter__/__defineSetter__,兼容性一些mozilla内核的浏览器

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineGetter

function defineReactive(obj, key, val){
obj.__defineGetter__(key, function() {
return val;
});
obj.__defineSetter__(key, function(newVal) {
val = newVal;
notifyChanges();
});
}

4、利用vbs,兼容性低版本的IE浏览器,IE11 edge不再支持(avalon

先window.execScript得到parseVB的方法

Function parseVB(code)
ExecuteGlobal(code)
End Function
window.execScript(parseVB_Code);

然后处理好数据属性properties生成get/set方法放在accessors,并把notifyChanges放到get/set中,然后动态生成以下vbs代码

Class DefinePropertyClass
Private [__data__], [__proxy__]
Public Default Function [__const__](d1, p1)
Set [__data__] = d1: set [__proxy__] = p1
Set [__const__] = Me
End Function
Public Property Let [bbb](val1)
Call [__proxy__](Me,[__data__], "bbb", val1)
End Property
Public Property Set [bbb](val1)
Call [__proxy__](Me,[__data__], "bbb", val1)
End Property
Public Property Get [bbb]
On Error Resume Next
Set[bbb] = [__proxy__](Me,[__data__],"bbb")
If Err.Number <> 0 Then
[bbb] = [__proxy__](Me,[__data__],"bbb")
End If
On Error Goto 0
End Property
Public Property Let [ccc](val1)
Call [__proxy__](Me,[__data__], "ccc", val1)
End Property
Public Property Set [ccc](val1)
Call [__proxy__](Me,[__data__], "ccc", val1)
End Property
Public Property Get [ccc]
On Error Resume Next
Set[ccc] = [__proxy__](Me,[__data__],"ccc")
If Err.Number <> 0 Then
[ccc] = [__proxy__](Me,[__data__],"ccc")
End If
On Error Goto 0
End Property
Public Property Let [$model](val1)
Call [__proxy__](Me,[__data__], "$model", val1)
End Property
Public Property Set [$model](val1)
Call [__proxy__](Me,[__data__], "$model", val1)
End Property
Public Property Get [$model]
On Error Resume Next
Set[$model] = [__proxy__](Me,[__data__],"$model")
If Err.Number <> 0 Then
[$model] = [__proxy__](Me,[__data__],"$model")
End If
On Error Goto 0
End Property
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]
End Class Function DefinePropertyClassFactory(a, b)
Dim o
Set o = (New DefinePropertyClass)(a, b)
Set DefinePropertyClassFactory = o;
End Function

执行以上两段vbs代码得到observable对象

window.parseVB(DefinePropertyClass_code);
window.parseVB(DefinePropertyClassFactory_code);
var vm = window.DefinePropertyClassFactory(accessors, VBMediator); function VBMediator(instance, accessors, name, value) {
var accessor = accessors[name]
if (arguments.length === 4) {
accessor.set.call(instance, value)
} else {
return accessor.get.call(instance)
}
}

5、在事件中触发检测digest,兼容所有(angular

以发XMLHttpRequest 为例

  var _XMLHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = function(flags) {
var req;
req = new _XMLHttpRequest(flags);
monitorXHR(req); //处理req绑定触发数据检测及notifyChanges处理
return req;
};

6、把数据转换成dom节点再利用defineProperty方法或onpropertychange事件,这种极端的办法主要是用来处理IE8的,因为IE8支持defineProperty但只有DOM元素才支持

function data2dom(obj,key,val){
if (!obj instanceof HTMLElement){
obj = document.createElement('i');
}
//defineProperty or onpropertychange handle
defineProperty(obj,key,val); //内部处理notifyChanges
return obj;
}

这种方法的成本开销是很大的

7、利用Object.observe,在Chrome 36 beta版本中出现,但很多浏览器还没有支持已从ES7草案中移除

var data = {};
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

//语法
var p = new Proxy(target, handler); //示例
let setter = {
set: function(obj, prop, value) {
obj[prop] = value;
notifyChanges();
}
}; let person = new Proxy({}, setter);
person.age = 28; //触发notifyChanges

9、利用脏检测,兼容所有,主要用于没有很好办法的情况下

利用脏检测实现Object.defineProperty方法

function PropertyChecker(obj, key, val, desc) {
this.key = key;
this.val = val;
this.get = function () {
var val = desc.get();
if (this.val == val) {
val = obj[key];
if (this.val != val) {
desc.set(val);
}
}
return val;
};
this.set = desc.set;
}
var checkList = [];
Object.defineProperty = function (obj, key, desc) {
var val = obj[key] = desc.value != undefined ? desc.value : desc.get();
if (desc.get && desc.set) {
var property = new PropertyChecker(obj, key, val, desc);
checkList.push(property);
}
}; function loopIE8() {
for (var i = 0; i < checkList.length; i++) {
var item = checkList[i];
var val = item.get();
if (item.val != val) {
item.val = val;
item.set(val);
}
}
}
setTimeout(function () {
setInterval(loopIE8, 200);
}, 1000);

四、监听数组变化

实际上以面说的这些仅仅是对数据对象进行监听,而数据中还包括数组,如:

var data = {a:[1,2,3]};
data.a.push(4);

这种操作也会使数据产生了变化,但是仅对getter setter进行定义是捕捉不到这些变化的。所以我们要单独针对数组做一些observable的处理。

基本思路就是重写数组的这些方法

1、push

2、pop,

3、shift

4、 unshift

5、splice

6、sort

7、reverse

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var arrayKeys = Object.keys(arrayMethods);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
var i = arguments.length;
var args = new Array(i);
while (i--) {
args[i] = arguments[i];
}
var result = original.apply(this, args);
var inserted;
switch (method) {
case 'push':
inserted = args;
break;
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) observe(inserted);
notifyChanges(); //通知变化
return result;
});
});
function def(obj, key, val, enumerable) {
obj = Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
function protoAugment(target, src) {
target.__proto__ = src;
} function copyAugment(target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
} var _augmentArr = ('__proto__' in {})? protoAugment : copyAugment;
function augmentArr(arr){
_augmentArr(arr, arrayMethods, arrayKeys);
};

使用时只需要调用augmentArr(arr)即可实现

五、数据监听存在哪些问题

目前主流的数据监听方案还是defineProperty + augmentArr的方式,已有不少的mvvm框架及一些observable类库,但是还存在一些问题:

1、所有的属性必须预先定义好

var data = new Observer({a:{b:1}});//这里没有定义a.c
data.$watch('a.c',function(newVal,oldVal){
console.log(arguments);
});
data.a.c = 1; //此时,监听a.c的watcher是不生效的,因为没有提前定义c属性

2、属性被覆盖后监听失效

var data = new Observer({a:{b:1}});
data.$watch('a.b',function(newVal,oldVal){
console.log(arguments);
});
data.a.b = 2; //生效
data.a = {b:3}; //此时b属性的原结构遭破坏,对b的监听失效

3、对数组元素的赋值是不会触发监听器更新的

var data = new Observer({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;
}
return this.splice(index, 1, val)[0];
});

4、删除对象的属性也不会触发监听器更新

var data = new Observer({a:{b:1},c:'xyz'});
data.$watch('a',function(newVal,oldVal){
console.log(arguments);
});
delete data.a; //不会触发a的watcher

同数组也可以父节点中定义一个$remove来实现

六、这些问题的解决方案

上述问题中:

1、第1、2其实是属于同一类的问题,就是因为这些notifyChanges直接在defineProperty时定义在属性中,当这个属性未定义或遭破坏时,那么对该属性的监听肯定是要失效的。对于这个问题的解决,我的思路是这样的

function Observer(data){
this.data = data;
var watches=[];
//监听时,先把监听数据保存在该observer实例的watches中
this.watch=function(path,subscriber,options){
watches.push(new Watcher(path,subscriber,options));
};
//当publish时把watcher转换为subscriber绑定到对应的属性上
this.publish = function(watch){
var target = queryProperty(watch.path);
var subscriber = new Subscriber(watch,target);
target.ob.subscribes.add(subscriber );
}
}

每当重新赋新值时,会从根节点拉取watches重新publish,这样的话保证了赋新值时原来的监听数据不会被覆盖。

var ob = new Observer(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中只需要修改一句代码就可以解决,也许是出于性能还其它的考虑它没有这么去做。即把数组的每个元素当做属性来定义

function observeArr(arr){
for (var i = 0, l = arr.length; i < l; i++) {
observeProperty(arr, i, arr[i]);
}
}

3、第4个问题除了父节点中增加$remove方法我目前也没有想到什么好的办法,如果大家有什么好的想法可以跟我交流下。

七、我对数据监听的实现

既然研究了下这个领域的东西,也就顺便造了个*实现了一个数据observable的功能,用法大概如下:

var data = {a:{b:{x:1,y:2}},c:[1,2,3]};
var ob = new Observer(data);
data.$watch('a.b',function(){
console.log(arguments);
},{deep:true})
data.a.b.x = 11;

主要是利用了es5的Object.defineProperty + augmentArr来实现的,代码400行左右。

https://github.com/liuhuisheng/actionjs/blob/master/src/observer.js

然后想支持下IE8写了个polifill,用脏检查实现了下

https://github.com/liuhuisheng/actionjs/blob/master/src/polifill.js

一直很懒终于总结了下做个笔记。

谈谈数据监听observable的实现的更多相关文章

  1. Python黑客编程基础3网络数据监听和过滤

    网络数据监听和过滤 课程的实验环境如下: •      操作系统:kali Linux 2.0 •      编程工具:Wing IDE •      Python版本:2.7.9 •      涉及 ...

  2. vue中的数据监听以及数据交互

    现在我们来看一下vue中的数据监听事件$watch, js代码: new Vue({ el:"#div", data:{ arr:[,,] } }).$watch("ar ...

  3. Vue获取dom和数据监听

    Vue获取dom对象 在js和jq中我们都能获取dom对象例如 // 获取id=1的div标签 <div id=d1>dom对象</div> // js语法 let ele = ...

  4. vue2&period;0 之计算属性和数据监听

    计算属性computed <template> <div> <input type="text" name="" v-model= ...

  5. Vue之数据监听存在的问题

    Vue之数据监听 当数据监听的是列表时,数据发生改变,不会被监听到. // 用$set修改数组中的数组能够被监听 // app.$set(this.hobby, 0, "爱你哦") ...

  6. 设置USB数据监听

    设置USB数据监听   在Kali Linux中,USB也是作为一个通信端口进行存在.常见的鼠标.键盘.U盘都是通过USB接口传输数据.所以,对于USB接口也可以实施监听,类似网络接口一样.在进行US ...

  7. Vue3 为何使用 Proxy 实现数据监听

    博客地址:https://ainyi.com/93 vue3 响应式数据放弃了 Object.defineProperty,而使用Proxy来代替它 我们知道,在 vue2 中,实现数据监听是使用Ob ...

  8. 1-STM32物联网开发WIFI&lpar;ESP8266&rpar;&plus;GPRS&lpar;Air202&rpar;系统方案安全篇&lpar;来看一下怎么样监听网络数据&comma;监听电脑上位机软件的数据&rpar;

    首先安装网络监听软件 运行这个软件 这个软件安装到电脑上,默认是监听咱电脑上的网络通信 咱们先监听电脑的软件的网络通信数据,然后再说怎么监听Wi-Fi和APP的软件的网络通信数据 咱就监听咱基础篇的 ...

  9. Vue -- 数据监听

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

随机推荐

  1. hadoop-hdfs分布式文件系统

    使用3台机器搭建hdfs完全分布式集群 201(NameNode),202(DataNode),203(DataNode) 整体架构 NameNode(192.168.1.201) DataNode( ...

  2. MVC教程

    http://developer.51cto.com/art/201309/409950_all.htm

  3. React 深入系列3:Props 和 State

    文:徐超,<React进阶之路>作者 授权发布,转载请注明作者及出处 React 深入系列3:Props 和 State React 深入系列,深入讲解了React中的重点概念.特性和模式 ...

  4. xslt 2&period;0 分组

    把数据拆成200个一组 <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet vers ...

  5. Java 内部类的作用

    1.内部类可以很好的实现隐藏 一般的非内部类,是不允许有 private 与protected权限的,但内部类可以 2.内部类拥有外围类的所有元素的访问权限 3.可是实现多重继承 4.可以避免修改接口 ...

  6. 再见VB6!再见程序生涯!

    今天写一篇博文告别使用了15年的VB6,从此终结程序生涯.记得读大学的时候第一学习编程,用VB6嵌入MediaPlayer控件写了一个简易视频播放器,看着视频的画面激动人心,从此就爱上了编程,从此也和 ...

  7. Cannot retrieve metalink for repository&colon; epel&sol;x86&lowbar;64&period; Please verify its path and try again 解决方法

    vim /etc/yum.repos.d/epel.repo 1 [epel] 2 name=Extra Packages for Enterprise Linux 7 - $basearch 3 # ...

  8. Condition线程通信(七)

    前言:对于线程通信,使用synchronized时使用wait.notify和notifyAll来实行线程通信.而使用Lock如何处理线程通信呢?答案就是本片的主角:Condition. 一.Cond ...

  9. Java中五种遍历HashMap的方式

    import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class Java8Templat ...

  10. Latency Compensating Methods in Client&sol;Server In-game Protocol Design and Optimization【转】

    https://developer.valvesoftware.com/wiki/Latency_Compensating_Methods_in_Client/Server_In-game_Proto ...