Ich bin auf diese Frage gestoßen, als ich nach etwas Ähnlichem suchte, aber ich denke, sie verdient eine gründliche Erklärung, was los ist, sowie einige zusätzliche Lösungen.
Wenn ein Angular-Ausdruck wie der, den Sie verwendet haben, im HTML vorhanden ist, richtet Angular automatisch eine $watch
für $scope.foo
und aktualisiert den HTML-Code immer dann, wenn $scope.foo
Änderungen.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
Der unausgesprochene Punkt ist, dass eines von zwei Dingen Auswirkungen hat aService.foo
so dass die Änderungen unentdeckt bleiben. Diese beiden Möglichkeiten sind:
aService.foo
wird jedes Mal auf ein neues Array gesetzt, wodurch der Verweis darauf veraltet ist.
aService.foo
wird so aktualisiert, dass ein $digest
Zyklus wird bei der Aktualisierung nicht ausgelöst.
Problem 1: Veraltete Referenzen
In Anbetracht der ersten Möglichkeit, unter der Annahme einer $digest
angewandt wird, wenn aService.foo
war immer das gleiche Array, die automatisch eingestellte $watch
würde die Änderungen erkennen, wie im folgenden Codeschnipsel gezeigt.
Lösung 1-a: Stellen Sie sicher, dass das Array oder Objekt die gleiches Objekt bei jeder Aktualisierung
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>
Wie Sie sehen können, ist das ng-repeat, das angeblich an aService.foo
wird nicht aktualisiert, wenn aService.foo
ändert, aber das ng-repeat, das an aService2.foo
tut . Dies liegt daran, dass unser Verweis auf aService.foo
ist veraltet, aber unser Verweis auf aService2.foo
ist nicht. Wir haben einen Verweis auf das ursprüngliche Array mit $scope.foo = aService.foo;
die dann vom Dienst bei der nächsten Aktualisierung verworfen wurde, was bedeutet $scope.foo
nicht mehr auf das gewünschte Array verweist.
Es gibt zwar mehrere Möglichkeiten, um sicherzustellen, dass der ursprüngliche Verweis beibehalten wird, aber manchmal kann es notwendig sein, das Objekt oder Array zu ändern. Oder was ist, wenn die Diensteigenschaft auf ein Primitivum wie ein String
oder Number
? In diesen Fällen können wir uns nicht einfach auf eine Referenz verlassen. Was also kann wir tun?
Mehrere der bereits gegebenen Antworten enthalten bereits einige Lösungen für dieses Problem. Ich persönlich bin jedoch für die Anwendung der einfachen Methode, die von Jin と thetallweeks in den Kommentaren:
referenzieren Sie einfach aService.foo in der html-Auszeichnung
Lösung 1-b: Fügen Sie den Dienst dem Bereich hinzu und verweisen Sie auf {service}.{property}
in der HTML-Datei.
Das heißt, tun Sie es einfach:
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>
Auf diese Weise kann die $watch
wird aufgelöst aService.foo
an jedem $digest
, die den korrekt aktualisierten Wert erhält.
Das ist in etwa das, was Sie mit Ihrer Umgehungslösung erreichen wollten, aber auf eine viel weniger umständliche Weise. Sie fügten eine unnötige $watch
im Controller, der explizit die foo
über die $scope
wenn sie sich ändert. Sie brauchen dieses Extra nicht $watch
wenn Sie die aService
anstelle von aService.foo
zum $scope
und binden Sie explizit an aService.foo
im Markup.
Das ist alles schön und gut, wenn man davon ausgeht, dass ein $digest
Zyklus angewandt wird. In meinen obigen Beispielen habe ich Angulars $interval
Dienst, um die Arrays zu aktualisieren, was automatisch eine $digest
Schleife nach jeder Aktualisierung. Was aber, wenn die Service-Variablen (aus welchem Grund auch immer) nicht innerhalb der "Angular-Welt" aktualisiert werden? Mit anderen Worten, wir nicht haben eine $digest
Zyklus automatisch aktiviert wird, wenn sich die Diensteigenschaft ändert?
Problem 2: Fehlen $digest
Viele der hier vorgestellten Lösungen werden dieses Problem lösen, aber ich stimme zu mit Code-Flüsterer :
Der Grund, warum wir ein Framework wie Angular verwenden, ist, dass wir uns keine eigenen Beobachtermuster ausdenken müssen.
Daher würde ich es vorziehen, weiterhin die aService.foo
Referenz im HTML-Markup, wie im zweiten Beispiel oben gezeigt, und müssen nicht einen zusätzlichen Callback innerhalb des Controllers registrieren.
Lösung 2: Verwenden Sie einen Setter und Getter mit $rootScope.$apply()
Ich war überrascht, dass noch niemand die Verwendung eines Setzer と getter . Diese Fähigkeit wurde mit ECMAScript5 eingeführt und ist somit schon seit Jahren bekannt. Das bedeutet natürlich, dass diese Methode nicht funktioniert, wenn Sie, aus welchem Grund auch immer, wirklich alte Browser unterstützen müssen, aber ich finde, dass Getter und Setter in JavaScript viel zu wenig genutzt werden. In diesem speziellen Fall könnten sie sehr nützlich sein:
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>
Hier habe ich eine "private" Variable in der Servicefunktion hinzugefügt: realFoo
. Diese wird aktualisiert und abgerufen mit der get foo()
と set foo()
Funktionen jeweils auf den service
Objekt.
Beachten Sie die Verwendung von $rootScope.$apply()
in der Set-Funktion. Dies stellt sicher, dass Angular über alle Änderungen an service.foo
. Wenn Sie "inprog"-Fehler erhalten, siehe diese nützliche Referenzseite , oder wenn Sie Angular >= 1.3 verwenden, können Sie einfach $rootScope.$applyAsync()
.
Seien Sie auch vorsichtig, wenn aService.foo
sehr häufig aktualisiert wird, da dies die Leistung erheblich beeinträchtigen könnte. Wenn die Leistung ein Problem wäre, könnten Sie ein Beobachter-Muster ähnlich wie die anderen Antworten hier mit dem Setter einrichten.