我偶然发现了这个问题,正在寻找类似的东西,但我认为它值得对正在发生的事情进行彻底的解释,以及一些额外的解决方案。
当 HTML 中出现您使用的 Angular 表达式时,Angular 会自动为 $scope.foo 设置一个 $watch,并在 $scope.foo 更改时更新 HTML。
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
这里未说明的问题是影响aService.foo 的两件事之一是无法检测到更改。这两种可能性是:
-
aService.foo 每次都被设置为一个新数组,导致对它的引用已过时。
-
aService.foo 的更新方式不会在更新时触发 $digest 循环。
问题 1:过时的引用
考虑第一种可能性,假设 $digest 被应用,如果 aService.foo 始终是同一个数组,则自动设置的 $watch 将检测到更改,如下面的代码 sn-p 所示。
解决方案 1-a:确保每次更新时数组或对象都是相同的对象
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.factory('aService2', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Keep the same array, just add new items on each update
$interval(function() {
if (service.foo.length < 10) {
service.foo.push(Math.random());
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
'aService2',
function FooCtrl($scope, aService, aService2) {
$scope.foo = aService.foo;
$scope.foo2 = aService2.foo;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in foo">{{ item }}</div>
<h1>Array is the same on each udpate</h1>
<div ng-repeat="item in foo2">{{ item }}</div>
</div>
</body>
</html>
如您所见,当aService.foo 更改时,应该附加到aService.foo 的ng-repeat 不会更新,但附加到aService2.foo 的ng-repeat 会。这是因为我们对aService.foo 的引用已经过时,但我们对aService2.foo 的引用却没有。我们使用 $scope.foo = aService.foo; 创建了对初始数组的引用,然后在下一次更新时被服务丢弃,这意味着 $scope.foo 不再引用我们想要的数组。
然而,虽然有多种方法可以确保初始引用保持完整,但有时可能需要更改对象或数组。或者,如果服务属性引用像 String 或 Number 这样的原语怎么办?在这些情况下,我们不能简单地依赖参考。那么我们可以做什么呢?
之前给出的几个答案已经为该问题提供了一些解决方案。不过,我个人更赞成在 cmets 中使用Jin 和thetallweeks 建议的简单方法:
只需在 html 标记中引用 aService.foo
解决方案 1-b:将服务附加到范围,并在 HTML 中引用 {service}.{property}。
意思,就是这样做:
HTML:
<div ng-controller="FooCtrl">
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
JS:
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js@1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
这样,$watch 将解析每个$digest 上的aService.foo,从而获得正确更新的值。
这是您尝试使用解决方法所做的一种方式,但方式要少得多。您在控制器中添加了一个不必要的$watch,它在更改时明确地将foo 放在$scope 上。当您将 aService 而不是 aService.foo 附加到 $scope 并在标记中显式绑定到 aService.foo 时,您不需要额外的 $watch。
假设正在应用$digest 循环,这一切都很好。在上面的示例中,我使用 Angular 的 $interval 服务来更新数组,每次更新后它会自动启动 $digest 循环。但是,如果服务变量(无论出于何种原因)没有在“Angular 世界”中得到更新怎么办。换句话说,我们不会在服务属性更改时自动激活$digest 循环?
这里的许多解决方案都会解决这个问题,但我同意Code Whisperer:
我们使用像 Angular 这样的框架的原因是为了不编写我们自己的观察者模式
因此,我更愿意继续在 HTML 标记中使用 aService.foo 引用,如上面第二个示例所示,而不必在 Controller 中注册额外的回调。
解决方案 2:使用带有 $rootScope.$apply() 的 setter 和 getter
我很惊讶还没有人建议使用setter 和getter。此功能是在 ECMAScript5 中引入的,因此已经存在多年。当然,这意味着如果出于某种原因,您需要支持非常旧的浏览器,那么这种方法将不起作用,但我觉得 getter 和 setter 在 JavaScript 中的使用率极低。在这种特殊情况下,它们可能非常有用:
factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// ...
}
angular.module('myApp', [])
.factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
setInterval(function() {
if (service.foo.length < 10) {
var newArray = [];
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Using a Getter/Setter</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
这里我在服务函数中添加了一个“私有”变量:realFoo。分别使用service 对象上的get foo() 和set foo() 函数更新和检索此get。
注意在 set 函数中使用$rootScope.$apply()。这确保了 Angular 将知道对 service.foo 的任何更改。如果您收到“inprog”错误,请参阅 this useful reference page,或者如果您使用 Angular >= 1.3,您可以使用 $rootScope.$applyAsync()。
如果aService.foo 的更新非常频繁,也请注意这一点,因为这可能会显着影响性能。如果性能是一个问题,您可以使用 setter 设置类似于此处其他答案的观察者模式。