談談數據監聽observable的實現

1、概述

數據監聽實現上就是當數據變化時會通知咱們的監聽器去更新全部的訂閱處理,如:javascript

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中的核心功能。這個功能咱們在不少場景中均可以用到,能夠大大的簡化咱們的代碼。css

2、現有MVVM框架中的Observable是怎麼實現的

先看看各MVVM框架對Observable是怎麼實現的,咱們分析下它們的實現原理,常見的MVVM框架有如下幾種:
一、knockout,老牌的MVVM實現html

<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函數,經過函數對該屬性進行取值賦值來實現的,缺點是改變了原屬性,不可以像屬性同樣取值賦值。vue

二、avalon,國產框架特色是兼容IE6+java

<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定義後的對象不可以動態增刪屬性。react

三、angular,大而全的mvvm解決方案git

<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(),缺點是須要對使用到的全部外部事件進行封裝。github

四、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。數組

3、Observable的實現有哪些方法及思路

經過上面幾個框架對比咱們能夠看出幾種不一樣數據監聽的實現方法,實際上還有不少的方式能夠去實現的:
一、把屬性轉換爲函數(knockout
二、IE9+使用defineProperty/definePropertiesvueavalon
三、低版本IE使用VBS(avalon
四、數據檢測,對各事件進行封裝,在封裝的方法中調用digest(angular
五、利用__defineGetter__/__defineSetter__方法(avalon
六、把數據轉換成dom對象利用IE8 dom對象的defineProperty方法或onpropertychange事件
七、利用Object.observe方法
八、利用ES6的Proxy對象
九、利用setInterval進行髒檢測

那麼咱們就具體看下這些數據監聽實現:
一、利用函數轉換如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); //賦值

二、利用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();
    }
  });
}

三、利用__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();
  });
}

四、利用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)
    }
}

五、在事件中觸發檢測digest,兼容全部(angular
以發XMLHttpRequest 爲例

var _XMLHttpRequest = window.XMLHttpRequest;
  window.XMLHttpRequest = function(flags) {
      var req;
      req = new _XMLHttpRequest(flags);
      monitorXHR(req); //處理req綁定觸發數據檢測及notifyChanges處理
      return req;
  };

六、把數據轉換成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;
}

這種方法的成本開銷是很大的

七、利用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);
    });
});

八、利用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

九、利用髒檢測,兼容全部,主要用於沒有很好辦法的狀況下
利用髒檢測實現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);

4、監聽數組變化

實際上以面說的這些僅僅是對數據對象進行監聽,而數據中還包括數組,如:

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

這種操做也會使數據產生了變化,可是僅對getter setter進行定義是捕捉不到這些變化的。因此咱們要單獨針對數組作一些observable的處理。

基本思路就是重寫數組的這些方法
一、push
二、pop,
三、shift
四、 unshift
五、splice
六、sort
七、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)便可實現

5、數據監聽存在哪些問題

目前主流的數據監聽方案仍是defineProperty + augmentArr的方式,已有很多的mvvm框架及一些observable類庫,可是還存在一些問題:
一、全部的屬性必須預先定義好

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屬性

二、屬性被覆蓋後監聽失效

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的監聽失效

三、對數組元素的賦值是不會觸發監聽器更新的

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];
});

四、刪除對象的屬性也不會觸發監聽器更新

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

同數組也能夠父節點中定義一個$remove來實現

6、這些問題的解決方案

上述問題中:
一、第一、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從新生效,這樣的話這個問題就能夠解決了。

二、第3個問題,其實很容易解決,好比vue中只須要修改一句代碼就能夠解決,也許是出於性能還其它的考慮它沒有這麼去作。即把數組的每一個元素當作屬性來定義

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

三、第4個問題除了父節點中增長$remove方法我目前也沒有想到什麼好的辦法,若是你們有什麼好的想法能夠跟我交流下。

7、我對數據監聽的實現

既然研究了下這個領域的東西,也就順便造了個輪子實現了一個數據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

一直很懶終於總結了下作個筆記。

相關文章
相關標籤/搜索