Ich bin auf diese Frage gestoßen, als ich nach etwas Ähnlichem gesucht habe, aber ich denke, sie verdient eine gründliche Erklärung der Vorgänge sowie einige zusätzliche Lösungen.
Wenn ein Winkelausdruck wie der von Ihnen verwendete im HTML-Code vorhanden ist, richtet Angular automatisch ein $watch
for ein $scope.foo
und aktualisiert den HTML-Code bei jeder $scope.foo
Änderung.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
Das unausgesprochene Problem hierbei ist, dass eines von zwei Dingen so wirkt aService.foo
, dass die Änderungen nicht erkannt werden. 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 $digest
beim Update kein Zyklus ausgelöst wird.
Problem 1: Veraltete Referenzen
Unter Berücksichtigung der ersten Möglichkeit würde unter der Annahme, dass a $digest
angewendet wird, wenn aService.foo
immer dasselbe Array verwendet würde, der automatisch festgelegte $watch
die Änderungen erkennen, wie im folgenden Codeausschnitt gezeigt.
Lösung 1-a: Stellen Sie sicher, dass das Array oder Objekt bei jedem Update dasselbe Objekt ist
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, wird die angeblich angehängte ng-Wiederholung aService.foo
bei aService.foo
Änderungen nicht aktualisiert , die angehängte ng-Wiederholung aService2.foo
jedoch . Dies liegt daran, dass unser Verweis auf aService.foo
veraltet ist, unser Verweis auf aService2.foo
jedoch nicht. Wir haben einen Verweis auf das ursprüngliche Array mit erstellt $scope.foo = aService.foo;
, der dann beim nächsten Update vom Dienst verworfen wurde, was bedeutet, dass $scope.foo
nicht mehr auf das gewünschte Array verwiesen wird.
Obwohl es verschiedene Möglichkeiten gibt, um sicherzustellen, dass die ursprüngliche Referenz erhalten bleibt, kann es manchmal erforderlich sein, das Objekt oder Array zu ändern. Oder was ist, wenn die Serviceeigenschaft auf ein Grundelement wie ein String
oder verweist Number
? In diesen Fällen können wir uns nicht einfach auf eine Referenz verlassen. Was können wir also tun?
Einige der zuvor gegebenen Antworten geben bereits einige Lösungen für dieses Problem. Ich persönlich bin jedoch dafür, die einfache Methode zu verwenden, die Jin und die ganzen Wochen in den Kommentaren vorgeschlagen haben:
Verweisen Sie einfach im HTML-Markup auf aService.foo
Lösung 1-b: Hängen Sie den Dienst an den Bereich an und verweisen Sie ihn {service}.{property}
im HTML-Code.
Das heißt, mach einfach das:
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 der $watch
Wille Entschlossenheit aService.foo
auf jeden $digest
, der die korrekt aktualisierte Wert erhalten wird.
Dies ist eine Art von dem, was Sie mit Ihrer Problemumgehung versucht haben, aber auf eine viel weniger runde Art und Weise. Sie hinzugefügt unnötig $watch
in der Steuerung , die legt ausdrücklich foo
das auf , $scope
wenn sie sich ändert. Sie brauchen nicht , dass zusätzliche , $watch
wenn Sie legen aService
statt aService.foo
auf das $scope
, und binden explizit aService.foo
im Markup.
Nun, das ist alles schön und gut, vorausgesetzt, ein $digest
Zyklus wird angewendet. In meinen obigen Beispielen habe ich Angulars $interval
Dienst verwendet, um die Arrays zu aktualisieren, wodurch $digest
nach jeder Aktualisierung automatisch eine Schleife ausgelöst wird. Was aber, wenn die Dienstvariablen (aus welchen Gründen auch immer) in der "Angular World" nicht aktualisiert werden? Mit anderen Worten, wir dont haben einen $digest
Zyklus automatisch aktiviert wird , sobald die Service - Eigenschaft ändert?
Problem 2: Vermisst $digest
Viele der hier aufgeführten Lösungen werden dieses Problem lösen, aber ich stimme Code Whisperer zu :
Der Grund, warum wir ein Framework wie Angular verwenden, besteht darin, keine eigenen Beobachtermuster zu erstellen
Daher würde ich es vorziehen, die aService.foo
Referenz im HTML-Markup weiterhin zu verwenden, wie im zweiten Beispiel oben gezeigt, und keinen zusätzlichen Rückruf im Controller registrieren zu müssen.
Lösung 2: Verwenden Sie einen Setter und Getter mit $rootScope.$apply()
Ich war überrascht, dass noch niemand die Verwendung von Setter und Getter vorgeschlagen hat . Diese Funktion wurde in ECMAScript5 eingeführt und gibt es daher seit Jahren. Das bedeutet natürlich, dass diese Methode nicht funktioniert, wenn Sie aus irgendeinem Grund wirklich alte Browser unterstützen müssen, aber ich denke, dass Getter und Setter in JavaScript stark unterbeansprucht 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 die Servicefunktion eingefügt : realFoo
. Dieses gets aktualisiert und abgerufen die Verwendung get foo()
und set foo()
Funktionen jeweils auf dem service
Objekt.
Beachten Sie die Verwendung von $rootScope.$apply()
in der eingestellten Funktion. Dadurch wird sichergestellt, dass Angular über Änderungen informiert wird service.foo
. Wenn Sie 'inprog'-Fehler erhalten, lesen Sie diese nützliche Referenzseite , oder wenn Sie Angular> = 1.3 verwenden, können Sie einfach verwenden $rootScope.$applyAsync()
.
Seien Sie auch vorsichtig, wenn aService.foo
diese sehr häufig aktualisiert werden, da dies die Leistung erheblich beeinträchtigen kann. Wenn die Leistung ein Problem darstellt, können Sie mit dem Setter ein Beobachtermuster einrichten, das den anderen Antworten hier ähnelt.