Je suis tombé sur cette question à la recherche de quelque chose de similaire, mais je pense qu'elle mérite une explication approfondie de ce qui se passe, ainsi que des solutions supplémentaires.
Lorsqu'une expression angulaire telle que celle que vous avez utilisée est présente dans le code HTML, Angular configure automatiquement un $watch
for $scope.foo
et met à jour le code HTML à chaque $scope.foo
modification.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
Le problème non dit ici est que l'une des deux choses affecte de aService.foo
telle sorte que les changements ne sont pas détectés. Ces deux possibilités sont:
aService.foo
est mis à un nouveau tableau à chaque fois, ce qui rend la référence obsolète.
aService.foo
est mis à jour de manière à ce $digest
qu'aucun cycle ne se déclenche sur la mise à jour.
Problème 1: Références obsolètes
Compte tenu de la première possibilité, en supposant que a $digest
est appliqué, s'il aService.foo
était toujours le même tableau, le paramètre défini automatiquement $watch
détecterait les modifications, comme indiqué dans l'extrait de code ci-dessous.
Solution 1-a: assurez-vous que le tableau ou l'objet est le même objet à chaque mise à jour
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>
Comme vous pouvez le voir, la répétition ng supposément attachée à aService.foo
ne se met pas à jour lors des aService.foo
changements, mais la répétition ng attachée à le aService2.foo
fait . C'est parce que notre référence à aService.foo
est dépassée, mais notre référence à aService2.foo
ne l'est pas. Nous avons créé une référence au tableau initial avec $scope.foo = aService.foo;
, qui a ensuite été supprimée par le service lors de sa prochaine mise à jour, ce qui signifie qu'il $scope.foo
ne faisait plus référence au tableau que nous voulions.
Cependant, bien qu'il existe plusieurs façons de s'assurer que la référence initiale est maintenue intacte, il peut parfois être nécessaire de modifier l'objet ou le tableau. Ou que faire si la propriété de service fait référence à une primitive comme un String
ou Number
? Dans ces cas, nous ne pouvons pas simplement nous fier à une référence. Alors, que pouvons- nous faire?
Plusieurs des réponses données précédemment donnent déjà des solutions à ce problème. Cependant, je suis personnellement en faveur de l'utilisation de la méthode simple suggérée par Jin et les haut - parleurs dans les commentaires:
il suffit de référencer aService.foo dans le balisage html
Solution 1-b: Attachez le service à l'étendue et référence {service}.{property}
dans le HTML.
Signification, faites juste ceci:
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>
De cette façon, le $watch
sera résolu aService.foo
sur chacun $digest
, ce qui obtiendra la valeur correctement mise à jour.
C'est un peu ce que vous essayez de faire avec votre solution de contournement, mais de manière beaucoup moins détournée. Vous avez ajouté un inutile $watch
dans le contrôleur qui met explicitement foo
le $scope
chaque fois qu'il change. Vous n'avez pas besoin de cet extra $watch
lorsque vous attachez aService
au lieu de aService.foo
à $scope
, et que vous vous liez explicitement à aService.foo
dans le balisage.
Maintenant, tout va bien en supposant qu'un $digest
cycle est appliqué. Dans mes exemples ci-dessus, j'ai utilisé le $interval
service d' Angular pour mettre à jour les tableaux, qui lance automatiquement une $digest
boucle après chaque mise à jour. Mais que faire si les variables de service (pour une raison quelconque) ne sont pas mises à jour dans le "monde angulaire". En d' autres termes, nous DonT avons un $digest
cycle étant activé automatiquement chaque fois que la propriété du service change?
Problème 2: manquant $digest
De nombreuses solutions ici permettront de résoudre ce problème, mais je suis d'accord avec Code Whisperer :
La raison pour laquelle nous utilisons un cadre comme Angular est de ne pas préparer nos propres modèles d'observateurs
Par conséquent, je préférerais continuer à utiliser la aService.foo
référence dans le balisage HTML comme indiqué dans le deuxième exemple ci-dessus, et ne pas avoir à enregistrer un rappel supplémentaire dans le contrôleur.
Solution 2: utilisez un setter et un getter avec $rootScope.$apply()
J'ai été surpris que personne n'ait encore suggéré l'utilisation d'un setter et d'un getter . Cette capacité a été introduite dans ECMAScript5 et existe donc depuis des années. Bien sûr, cela signifie que si, pour une raison quelconque, vous devez prendre en charge de très anciens navigateurs, cette méthode ne fonctionnera pas, mais j'ai l'impression que les getters et setters sont largement sous-utilisés en JavaScript. Dans ce cas particulier, ils pourraient être très utiles:
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>
Ici , j'ai ajouté une variable « privée » dans la fonction de service: realFoo
. Ce get est mis à jour et récupéré à l'aide des fonctions get foo()
et set foo()
respectivement sur l' service
objet.
Notez l'utilisation de $rootScope.$apply()
dans la fonction set. Cela garantit qu'Angular sera au courant de toute modification apportée à service.foo
. Si vous obtenez des erreurs «inprog», consultez cette page de référence utile , ou si vous utilisez Angular> = 1.3, vous pouvez simplement l'utiliser $rootScope.$applyAsync()
.
Méfiez-vous également de cela si la aService.foo
mise à jour est très fréquente, car cela pourrait avoir un impact significatif sur les performances. Si les performances sont un problème, vous pouvez configurer un modèle d'observateur similaire aux autres réponses ici à l'aide du setter.