我想出了三种不同的方法来实现计算值的双向绑定。我只为assets 滑块而不是roa 滑块编写了解决方案,因为我assets 是一个基本的总和,而roa 是另外一回事。 (@flashpunk:如果您在问题的这方面需要更多帮助,请解释一下。)为什么这很困难的关键问题是,在 javascript 中,数字不是以精确的 base 10 格式表示的;当遇到这种不精确的损失时,angular会进入一个无限循环,根据不精确引起的变化重新计算变化。
换句话说,问题在于数值计算是有损的,如果计算是无损就不会出现这些问题。
这是我的第一个解决方案。它使用(脆弱的)锁定机制来根据更改的值切换规范数据源。为了使这个解决方案更加健壮,我将创建一个扩展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;
});
在这个版本中,我避免了笨拙的锁定机制,并将观察者与当前可用的 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];
});
这可能是我一开始就应该建议的。拥有一个规范数据源,即单个滑块。对于主滑块,创建一个自定义滑块指令,而不是使用 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 decimal 或 this, for fractional)。 -- https://www.google.com/search?q=javascript+number+libaries