如何知道哪个对象属性发生了改变?

6

我有一个包含10个对象的地图。每个对象都有10个属性val1,...,val10。现在,要检查地图中特定对象的特定属性是否已更改,我需要编写 $scope.$watch 100 次。如何监视整个地图/JS对象并确切地知道哪个属性已更改?

/*watch individual property*/
            $scope.$watch('map[someid].val1', function(new_val, old_val)
            {
                //do something
            });

    /*watch entire object, how would this help me?*/
            $scope.$watch('map', function(new_val, old_val)
            {
                //do something
            }, true);

我建议使用一些面向切面编程(AOP)库: https://dev59.com/UHNA5IYBdhLWcg3wVcP6 - the_marcelo_r
2
尝试使用watchCollection。http://docs.angularjs.org/api/ng/type/$rootScope.Scope - mainguy
这是一个有趣的问题。请在找到解决方案后发布它。那将非常棒。谢谢! - pj013
6个回答

5
由于之前的答案似乎暗示AngularJS没有为您的问题提供有针对性的解决方案,因此让我使用原生JS提出一个解决方案。
据我所知,如果您想要实现对象的监视器,您将需要某种形式的getter/setter模式(又名Mutator method)。我不知道任何关于AngularJS的事情,但我非常确定他们也必须使用类似的东西。在JavaScript中实现getter和setter有多种方法(请参见上面链接的维基百科页面),但通常有三个主要选择:
- 使用外部处理程序(完全隐藏内部数据结构的对象,并仅提供用于读取和写入的API) - 使用Java风格的getSomething和setSomething方法 - 使用ECMAScript 5.1引入的本机JavaScript属性getter和setter 出于某种原因,我看到了更多通过前两种方法来执行它的示例,但另一方面,我看到了一个使用第三种方法构建的相当复杂和美丽的API的示例。我将通过使用Object.defineProperty给您提供一个通过第三种方法解决您的问题的示例:

这个想法是提供一个观察函数,然后作为属性设置器的一部分来调用它:

function watcher(objectIndex, attributeName) {
  console.log('map[' + objectIndex + '].' + attributeName + ' has changed');
}

Object.defineProperty有点像以复杂的方式为对象定义属性。因此,当您通常会编写以下代码时

obj.val1 = "foo";

你将编写

Object.defineProperty(obj, 'val1', {
  get: function ()         { /* ... */ },
  set: function (newValue) { /* ... */ },
  // possible other parameters for defineProperty
})

比如说,当 obj.val1 被读取或写入时,您可以定义希望执行的操作。这使您有可能在 setter 的一部分中调用您的 watcher 函数:

Object.defineProperty(obj, 'val1', {
  get: function ()         { /* ... */ },
  set: function (newValue) {
    // set the value somewhere; just don't use this.val1
    /* call the */ watcher(/* with args, of course */)
  }
})

虽然这似乎是一段相当简单的代码,但你可以使用函数式编程技术和闭包来构建程序,使其实际上并不那么复杂。因此,即使这看起来不像是“AngularJS方式”做事情,手动实现也不会增加太多开销。
以下是针对您问题的完整代码示例:http://codepen.io/cido/pen/DEpLj/ 在示例中,监视器函数被硬编码为数据结构实现的一部分。但是,如果您需要在多个地方重用此代码,则没有任何阻止您使用例如addEventListener风格的挂钩等扩展解决方案。
由于此方法非常使用闭包,因此如果您真的处理大量数据,则可能会有一些内存影响。但是,在大多数用例中,您无需担心这种类型的问题。另一方面,所有基于“深度相等”检查的解决方案都不会超级内存高效或快速。

据我所知,Angular 使用原生 JS 对象,没有 getter 和 setter 函数,但有一个脏检查循环。 - caffeinated.tech
因此,换句话说:“不要使用Angular来检测更改,而是使用您的对象模型。”这不是一个坏主意。Angular并不一定需要控制应用程序中发生的所有事情。只需要控制应用程序的用户界面即可。 - XML
我已经编写了一个纯JavaScript函数,可以与Angular的$watch一起使用,以便执行此操作。请检查我的答案。 - a11smiles

4

目前在AngularJS中没有内置的方法来为您处理此操作。

您需要自己实现或查找可为您完成此操作的库。

如果您希望使用任何内置的监视器函数来自行实现,有以下三个选项:

1. $watch并将false作为第三个参数:比较引用相等性。在您的情况下不是一个选择。

2. $watchCollection:将浅层监视映射中的十个对象。在您的情况下也不是一个选择。

3. $watch并将true作为第三个参数:将使用angular.equals进行对象相等性比较。适用于您的用例。

以下是一个示例,它使用true作为第三个参数注册了整个映射的监视器,并手动执行了比较。

注意:根据您的应用程序(绑定和调用$digest的数量等),监视大型复杂对象可能会导致不良的内存和性能影响。由于此示例使用了另一个angular.copy($watch至少会在内部使用一个),因此会对性能产生更大的影响。

$scope.map = {};

for (var i = 0; i < 3; i++) {
  $scope.map[i] = {
    property1: 'value1',
    property2: 'value2',
    property3: 'value3'
  };
}

var source = [];

var updateSource = function(newSource) {
  angular.copy(newSource, source);
};

updateSource($scope.map);

$scope.$watch("map", function(newMap, previousMap) {

  if (newMap !== previousMap) {

    for (var id in newMap) {

      if (angular.equals(newMap[id], source[id])) continue;

      var newObject = newMap[id];
      var previousObject = source[id];

      for (var property in newObject) {

        if (!newObject.hasOwnProperty(property) ||
          angular.equals(newObject[property], previousObject[property])) continue;

        var cd = $scope.changeDetection = {};

        cd.changedObjectId = id;
        cd.previousObject = previousObject;
        cd.newObject = newObject;
        cd.changedProperty = property;
        cd.previousPropertyValue = previousObject[property];
        cd.newPropertyValue = newObject[property];
      }

      updateSource(newMap);
    }
  }
}, true);

示例:http://plnkr.co/edit/MtfJTY3D4osUtXtIcCiR?p=preview

这个示例展示了一个基本的IT技术应用。请点击链接查看示例。

2
我会尝试从Angular文档中使用这个。
$scope.$watchCollection(collection, function(newValue, oldValue) {
 //do stuff when collection changes
});

http://docs.angularjs.org/api/ng/type/$rootScope.Scope


$watchcollection不会$watch嵌套对象属性。 - bluehallu
@Hallucynogenyc你能证明你的说法吗? - Wawy
@Wawy 当然,看一下文档:$watchCollection(obj, listener); 浅层监视对象的属性。 - bluehallu

2
$watch有一个不太为人所知的第三个参数,如果设置为true,将使用angular.equals()而不是使用===来测试相等性。现在angular.equals()会考虑两个对象相等,如果一组条件中的任何一个条件成立,其中一个条件是:
  • 两个对象或值具有相同的类型,并且它们的所有属性都可以通过使用angular.equals进行比较而相等。
这基本上意味着它将执行递归(深层)比较,这正是你需要的。因此,检测任何嵌套属性的更改就像这样简单:
scope.$watch('map', function (newmap,oldmap) { 
  // detect what changed manually with your own recursive function
}, true);

这不会告诉你发生了什么变化,所以需要你自己进行处理。考虑直接将你的视图绑定到map上,并确保所有更改都在Angular $digest周期内发生,或者将其包装在$apply中,以便更轻松地解决问题。如果你的用例必须使用$watch,你还可以递归迭代对象并在其中设置单独的$watch。


1
我知道我来晚了,但我需要类似的东西,上面的答案没有帮助到我。
我正在使用Angular的$watch函数来检测变量的更改。我不仅需要知道变量上的属性是否已更改,还想确保更改的属性不是临时计算字段。换句话说,我想忽略某些属性。
这是代码:https://jsfiddle.net/rv01x6jo/ 以下是如何使用它:
$scope.$watch($scope.MyObjectToWatch, function(newValue, oldValue) {

    // To only return the difference
    var difference = diff(newValue, oldValue);  

    // To exclude certain properties from being evaluated (i.e. temporary/calculated/meta properties)
    var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);

    if (Object.keys(difference).length == 0) 
        return;  // Nothing has changed
    else
        doSomething();  // Something has changed

});

通过检查 Object.keys(difference) 数组,您可以看到所有已更改的属性。

希望这能帮助到某些人。


0
一种方法是编写一个小函数,可以将对象nowthen展平并形成字符串。从那时起,比较字符串并找到不匹配点就很简单了,这也是对象发生更改的点。

不用说,所有对象属性应该被公开

var flatten = function (key, obj) {
    var ret = '';
    $.each(obj, function (_key, val) {
        if (typeof val == 'object') {
            ret += flatten(key + '.' + _key, val);
        } else {
            ret += ((key ? key + '.' : '') + _key + ':' + val + ',');
        }
    });
    return ret;
};

Fiddle

接下来可能是这样的:

$scope.$watch(obj, function (now, then) {
    var nowFlat = flatten(now),
        thenFlat = flatten(then);
    compareStrings(nowFlat, thenFlat); // add custom string compare function here
}, true)

作为性能优化,您可以始终将nowFlat缓存并在下次触发$watch时使用它作为thenFlat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接