Mi sono imbattuto in questa domanda alla ricerca di qualcosa di simile, ma penso che meriti una spiegazione approfondita di ciò che sta accadendo, nonché alcune soluzioni aggiuntive.
Quando un'espressione angolare come quella che hai usato è presente nell'HTML, Angular imposta automaticamente un $watch
per $scope.foo
e aggiornerà l'HTML ogni volta che $scope.foo
cambia.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
Il problema non detto qui è che una delle due cose sta interessando in modo aService.foo
tale che i cambiamenti non vengano rilevati. Queste due possibilità sono:
aService.foo
viene impostato su un nuovo array ogni volta, rendendo obsoleto il riferimento ad esso.
aService.foo
viene aggiornato in modo tale che $digest
non venga attivato un ciclo sull'aggiornamento.
Problema 1: riferimenti obsoleti
Considerando la prima possibilità, supponendo che $digest
venga applicato a, se aService.foo
fosse sempre lo stesso array, l'impostazione automatica $watch
rileverà le modifiche, come mostrato nello snippet di codice di seguito.
Soluzione 1-a: assicurarsi che l'array o l'oggetto sia lo stesso oggetto su ogni aggiornamento
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>
Come puoi vedere, la ng-repeat presumibilmente allegata aService.foo
non si aggiorna quando aService.foo
cambia, ma la ng-repeat allegata aService2.foo
fa . Questo perché il nostro riferimento a non aService.foo
è aggiornato, ma il nostro riferimento aService2.foo
non lo è. Abbiamo creato un riferimento all'array iniziale con $scope.foo = aService.foo;
, che è stato quindi scartato dal servizio al suo prossimo aggiornamento, il che significa che $scope.foo
non faceva più riferimento all'array che desideravamo più.
Tuttavia, mentre ci sono diversi modi per assicurarsi che il riferimento iniziale sia mantenuto intatto, a volte può essere necessario cambiare l'oggetto o l'array. O se la proprietà del servizio fa riferimento a una primitiva come a String
o Number
? In questi casi, non possiamo semplicemente fare affidamento su un riferimento. Allora, cosa possiamo fare?
Molte delle risposte fornite in precedenza forniscono già alcune soluzioni a questo problema. Tuttavia, sono personalmente favorevole all'utilizzo del metodo semplice suggerito da Jin e dalle settimane successive nei commenti:
fai semplicemente riferimento aService.foo nel markup html
Soluzione 1-b: collegare il servizio all'ambito e fare riferimento {service}.{property}
in HTML.
Significato, basta fare questo:
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>
In questo modo, $watch
verrà risolto aService.foo
su ciascuno $digest
, che otterrà il valore aggiornato correttamente.
Questo è un po 'quello che stavi cercando di fare con la soluzione alternativa, ma in modo molto meno circolare. È stato aggiunto un elemento superfluo $watch
nel controller che foo
attiva esplicitamente $scope
ogni volta che cambia. Non hai bisogno di quel extra $watch
quando ti colleghi aService
invece aService.foo
al $scope
, e ti leghi esplicitamente al aService.foo
markup.
Ora va tutto bene, supponendo che $digest
venga applicato un ciclo. Nei miei esempi precedenti, ho usato il $interval
servizio di Angular per aggiornare gli array, che avvia automaticamente un $digest
loop dopo ogni aggiornamento. Ma cosa succede se le variabili di servizio (per qualsiasi motivo) non vengono aggiornate all'interno del "mondo angolare". In altre parole, non$digest
viene attivato automaticamente un ciclo ogni volta che cambia la proprietà del servizio?
Problema 2: mancante $digest
Molte delle soluzioni qui risolveranno questo problema, ma sono d'accordo con Code Whisperer :
Il motivo per cui stiamo usando un framework come Angular è di non preparare i nostri schemi di osservatori
Pertanto, preferirei continuare a utilizzare il aService.foo
riferimento nel markup HTML come mostrato nel secondo esempio sopra e non dovrei registrare un callback aggiuntivo all'interno del controller.
Soluzione 2: utilizzare un setter e getter con $rootScope.$apply()
Sono stato sorpreso che nessuno abbia ancora suggerito l'uso di un setter e getter . Questa funzionalità è stata introdotta in ECMAScript5 e esiste da anni ormai. Naturalmente, ciò significa che, per qualsiasi motivo, è necessario supportare browser molto vecchi, questo metodo non funzionerà, ma mi sento come se i getter e i setter fossero ampiamente sottoutilizzati in JavaScript. In questo caso particolare, potrebbero essere abbastanza utili:
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>
Qui ho aggiunto una variabile 'privato' nella funzione di servizio: realFoo
. Questo viene aggiornato e recuperato usando rispettivamente le funzioni get foo()
e sull'oggetto.set foo()
service
Notare l'uso di $rootScope.$apply()
nella funzione impostata. Ciò garantisce che Angular sia a conoscenza di eventuali modifiche a service.foo
. Se ricevi errori 'inprog', vedi questa utile pagina di riferimento o se usi Angular> = 1.3 puoi semplicemente usare $rootScope.$applyAsync()
.
Diffidare anche di questo se aService.foo
viene aggiornato molto frequentemente, poiché ciò potrebbe influire in modo significativo sulle prestazioni. Se le prestazioni rappresentano un problema, è possibile impostare un modello di osservatore simile alle altre risposte qui utilizzando il setter.