【问题标题】:AngularJs $watch with 2 way bindingAngularJs $watch 与 2 方式绑定
【发布时间】:2013-10-11 18:30:25
【问题描述】:

我正在创建一个页面,其中的范围滑块会根据它们的设置相互影响。我有一个如下所示的监视功能,它以一种方式工作,但是我希望两种方式都具有监视效果,但是我不知道是否有一种方法可以做到这一点,而无需为每个值创建一个(大约有 12 个滑块)。

这是一个 Plunker 概述了我正在尝试做的事情: http://plnkr.co/edit/CXMeOT?p=preview

【问题讨论】:

  • 抱歉,已将 jquery 标签移除到条目中
  • 很难掌握你的特殊情况;链接到 jsfiddle(或任何其他类似工具)会有所帮助。
  • 如果assetsroa 发生变化,您希望手表触发吗?
  • @forivall 我已经向 OP 添加了一个 plunker - 感谢您的建议
  • @flashpunk 看看我的编辑。

标签: javascript angularjs


【解决方案1】:

我想出了三种不同的方法来实现计算值的双向绑定。我只为assets 滑块而不是roa 滑块编写了解决方案,因为我assets 是一个基本的总和,而roa 是另外一回事。 (@flashpunk:如果您在问题的这方面需要更多帮助,请解释一下。)为什么这很困难的关键问题是,在 javascript 中,数字不是以精确的 base 10 格式表示的;当遇到这种不精确的损失时,angular会进入一个无限循环,根据不精确引起的变化重新计算变化。

换句话说,问题在于数值计算是有损的,如果计算是无损就不会出现这些问题。

Approach 1 (click for plunkr)

这是我的第一个解决方案。它使用(脆弱的)锁定机制来根据更改的值切换规范数据源。为了使这个解决方案更加健壮,我将创建一个扩展Scope 的角度模块,以允许更高级和更复杂的观察者。如果以后遇到问题,我可能会在github上启动一个项目;如果我这样做,我会修改这个答案。

// Both of these functions can be replaced with library functions from lodash or underscore, etc.
var sum = function(array) { return array.reduce(function(total, next) { return total + (+next); }, 0) };
var closeTo = function(a, b, threshold) { return a == b || Math.abs(a - b) <= threshold; };

$scope.sliders = [
$scope.slide1 = {
  assets: 10,
  roa: 20
}, ... ];

$scope.mainSlide = {
  assets: 0,
  roa: 0
};
// you might want to make or look for a helper function to control this "locking"
// in a better fashion, that integrates and resets itself with the digest cycle
var modifiedMainAssets = false;
var modifiedCollectionAssets = false;
$scope.$watch(function() {
      return sum($scope.sliders.map(function(slider) { return slider.assets; }))
    }, function(totalAssets) {
  if (modifiedCollectionAssets) { modifiedCollectionAssets = false; return; }
  $scope.mainSlide.assets = totalAssets;
  modifiedMainAssets = true;
});
$scope.$watch('mainSlide.assets', function(totalAssets, oldTotalAssets) {
  if (modifiedMainAssets) { modifiedMainAssets = false; return; }
  if (oldTotalAssets === null || closeTo(totalAssets, oldTotalAssets, 0.1)) { return; }

  // NOTE: has issues with floating point math. either round & accept the failures,
  // or use a library that supports a proper IEEE 854 decimal type
  var incrementAmount = (totalAssets - oldTotalAssets) / $scope.sliders.length;
  angular.forEach($scope.sliders, function(slider) { slider.assets += incrementAmount; });
  modifiedCollectionAssets = true;
});

Approach 2

在这个版本中,我避免了笨拙的锁定机制,并将观察者与当前可用的 Angular 统一起来。我使用单个滑块的值作为规范数据源,并根据变化量重新计算总数。同样,内置工具不足以彻底表达问题,我需要手动保存oldValues。 (代码可能更简洁,但我没想到必须执行上述操作)。

$scope.calculateSum = function() {
  return sum($scope.sliders.map(function(slider) { return slider.assets; }));
};
$scope.mainSlide.assets = $scope.calculateSum();
var modifiedMainAssets = false;
var modifiedCollectionAssets = false;
var oldValues = [$scope.mainSlide.assets, $scope.mainSlide.assets];
$scope.$watchCollection('[calculateSum(), mainSlide.assets]'
    , function(newValues) {
  var newSum = newValues[0];
  var oldSum = oldValues[0];
  var newAssets = newValues[1];
  var oldAssets = oldValues[1];
  if (newSum !== oldSum) {
    $scope.mainSlide.assets = newSum;
  }
  else if (newAssets !== oldAssets) {
    var incrementAmount = (newAssets - oldAssets) / $scope.sliders.length;
    angular.forEach($scope.sliders, function(slider) { slider.assets += incrementAmount; });
    $scope.mainSlide.assets = $scope.calculateSum();
  }
  oldValues = [$scope.mainSlide.assets, $scope.mainSlide.assets];
});

Approach 3: Back to basics

这可能是我一开始就应该建议的。拥有一个规范数据源,即单个滑块。对于主滑块,创建一个自定义滑块指令,而不是使用 ng-model,触发报告所做更改的事件。 (您可以包装或分叉现有指令来执行此操作。)

$scope.$watch('calculateSum()', function(newSum, oldSum) {
    $scope.mainSlide.assets = newSum;
});
$scope.modifyAll = function(amount) {    
    angular.forEach($scope.sliders, function(slider) { slider.assets += amount; });
};

修改后的html:

Assets: <span ng-bind="mainSlide.assets"></span>
<button ng-click="modifyAll(1)">Up</button>
<button ng-click="modifyAll(-1)">Down</button>

评论

这似乎是 Angular 团队尚未解决的问题。基于事件的数据绑定系统(用于淘汰赛、ember 和骨干网)将通过简单地不对有损的特定计算更改触发更改事件来解决此问题;在 Angular 的情况下,需要对摘要循环进行一些调整。我不知道我的前两个解决方案是否是 Misko 或其他角度大师的建议,但它们有效。

目前,我更喜欢方法 3。如果您不满意,请使用方法 2,然后清理它;但使用无损小数或分数表示,而不是内置的 javascript Number 类型(如 for decimalthis, for fractional)。 -- https://www.google.com/search?q=javascript+number+libaries

【讨论】:

  • 在 OP 中添加了一个 plunker。我正在使用角度 1.1.5。
  • 这是你写的一段令人难以置信的代码,但我认为它没有考虑到向下递增的公式?
  • @flashpunk: "increment down" 你是什么意思?另外,我进一步编辑并扩展了我的答案。第三种方法使用 jsfiddle 而不是 plunkr,因为 plunkr 已关闭。
【解决方案2】:

我相信这就是你想要的。

$scope.watchList = [$scope.item1, $scope.item2];
$scope.$watchCollection('watchList', function(newValues){...});

这样您就可以在同一个手表中查看所有滑块范围变量。

【讨论】:

  • 它不起作用,因为$watchCollection 只观察一层深度,所以它不会检测对象内部的变化,即使对象标识发生变化,或者项目被添加/删除。您可以在 $watch 上使用 equality 参数(将第三个参数设置为 true)。
【解决方案3】:

在两个方向上创建 $watch 有什么问题?如果您在这里有 12 种不同的情况,我建议您构建一个小工厂功能来设置它们。您只需要确保您的计算值稳定,否则每个$watch 语句都会触发另一个语句,从而导致无限递归。

我猜测您在滑块之间的依赖关系有点,但我建议如下:

function bindSliders() {

    var mainSlider = arguments[0];
    var childSliders = arguments.slice(1);

    angular.forEach(childSliders, function(slider) {

        // Forward Direction
        $scope.$watch(function() {
            return slider.roa + slider.assets;
        }, function (newValue, oldValue) {
            // A child slider changed
            // Sum up all the child sliders and update mainSlider
        });

        // Reverse Direction
        $scope.$watch(function() {
            return mainSlider.assett + mainSlider.roa
        }, function (newValue, oldValue) {
            // Update this `slider` based on the updated main value

        });

    });
}

bindSliders($scope.mainSlide, $scope.slide1, $scope.slide2, $scope.slide3, $scope.slide4);

【讨论】:

  • 在 OP 中添加了一个 plunker。
  • 这是个好主意,但我提供的原始示例已大大简化。我认为使用你的方法,我必须有太多的工厂。
  • 您只需要参数化您的计算和绑定。真的不需要超过1个。您的 plunker 一遍又一遍地重复相同的计算(slide1 + slide2 ...,等等)。将它们分成可以重复使用的函数。
猜你喜欢
  • 1970-01-01
  • 2013-11-29
  • 1970-01-01
  • 1970-01-01
  • 2012-10-23
  • 2014-02-13
  • 1970-01-01
  • 2016-02-27
  • 2014-03-10
相关资源
最近更新 更多