Natknąłem się na to pytanie, szukając czegoś podobnego, ale myślę, że zasługuje na dokładne wyjaśnienie tego, co się dzieje, a także kilka dodatkowych rozwiązań.
Gdy wyrażenie HTML, takie jak użyte, jest obecne w kodzie HTML, Angular automatycznie konfiguruje $watch
for $scope.foo
i będzie aktualizować HTML przy każdej $scope.foo
zmianie.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
Niewypowiedzianym problemem jest to, że jedna z dwóch rzeczy wpływa na to aService.foo
, że zmiany nie są wykrywane. Te dwie możliwości to:
aService.foo
ustawia się za każdym razem do nowej tablicy, powodując, że odwołanie do niej jest nieaktualne.
aService.foo
jest aktualizowany w taki sposób, że $digest
cykl nie jest uruchamiany podczas aktualizacji.
Problem 1: Nieaktualne referencje
Biorąc pod uwagę pierwszą możliwość, zakładając, że $digest
stosuje się a, jeśli aService.foo
zawsze była to ta sama tablica, zestaw automatycznie $watch
wykryłby zmiany, jak pokazano w poniższym fragmencie kodu.
Rozwiązanie 1-a: Upewnij się, że tablica lub obiekt jest tym samym obiektem przy każdej aktualizacji
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>
Jak widać, NG-repeat rzekomo przypisane do aService.foo
nie aktualizuje kiedy aService.foo
zmiany, ale ng-repeat dołączone do aService2.foo
robi . Jest tak, ponieważ nasze odniesienie do aService.foo
jest nieaktualne, ale nasze odniesienie do aService2.foo
nie jest. Utworzyliśmy odwołanie do początkowej tablicy $scope.foo = aService.foo;
, która została następnie odrzucona przez usługę przy następnej aktualizacji, co oznacza, że $scope.foo
nie odnosi się już do tablicy, której chcieliśmy.
Jednakże, chociaż istnieje kilka sposobów, aby upewnić się, że początkowe odniesienie jest utrzymywane w takcie, czasami może być konieczna zmiana obiektu lub tablicy. A co jeśli właściwość usługi odwołuje się do prymitywu takiego jak String
lub Number
? W takich przypadkach nie możemy po prostu polegać na referencji. Więc co można zrobić?
Kilka podanych wcześniej odpowiedzi już daje pewne rozwiązania tego problemu. Jednak osobiście opowiadam się za zastosowaniem prostej metody sugerowanej przez Jina i thetallweeks w komentarzach:
po prostu odwołaj aService.foo w znaczniku HTML
Rozwiązanie 1-b: Dołącz usługę do zakresu i odwołanie {service}.{property}
w kodzie HTML.
Czyli po prostu zrób to:
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>
W ten sposób $watch
zostanie rozwiązany aService.foo
każdy $digest
, który uzyska poprawnie zaktualizowaną wartość.
Jest to rodzaj tego, co próbujesz zrobić ze swoim obejściem, ale w znacznie mniejszym stopniu. Dodałeś niepotrzebne $watch
w kontrolerze, który jawnie nakłada foo
się za $scope
każdym razem, gdy się zmienia. Nie potrzebujesz tego dodatkowego, $watch
gdy dołączasz aService
zamiast aService.foo
do $scope
i przypisujesz się bezpośrednio do aService.foo
znaczników.
To wszystko dobrze i dobrze, jeśli zastosuje się $digest
cykl. W powyższych przykładach użyłem $interval
usługi Angulara do aktualizacji tablic, która automatycznie uruchamia $digest
pętlę po każdej aktualizacji. Ale co, jeśli zmienne serwisowe (z jakiegokolwiek powodu) nie są aktualizowane w „świecie Angular”. Innymi słowy, czy nie mamy $digest
cyklu aktywowanego automatycznie przy każdej zmianie właściwości usługi?
Problem 2: Brak $digest
Wiele rozwiązań tutaj rozwiązuje ten problem, ale zgadzam się z Code Whisperer :
Powodem, dla którego używamy frameworku takiego jak Angular, jest nie gotowanie własnych wzorców obserwatorów
Dlatego wolałbym nadal używać aService.foo
odwołania w znacznikach HTML, jak pokazano w drugim przykładzie powyżej, i nie musiałbym rejestrować dodatkowego wywołania zwrotnego w kontrolerze.
Rozwiązanie 2: Użyj setera i gettera z $rootScope.$apply()
Byłem zaskoczony, że nikt jeszcze nie zasugerował użycia setera i gettera . Ta funkcja została wprowadzona w ECMAScript5 i dlatego istnieje już od lat. Oczywiście oznacza to, że jeśli z jakiegoś powodu musisz obsługiwać naprawdę stare przeglądarki, ta metoda nie będzie działać, ale wydaje mi się, że JavaScript i gettery są znacznie słabo wykorzystywane. W tym konkretnym przypadku mogą być bardzo przydatne:
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>
Tutaj dodałem „prywatny” zmienną w funkcji serwisowej: realFoo
. Ten plik jest aktualizowany i pobierany przy użyciu odpowiednio funkcji get foo()
i set foo()
na service
obiekcie.
Zwróć uwagę na użycie $rootScope.$apply()
funkcji set. Zapewnia to, że Angular będzie świadomy wszelkich zmian w service.foo
. Jeśli pojawią się błędy „inprog”, zobacz tę przydatną stronę referencyjną lub jeśli używasz Angular> = 1.3, możesz po prostu użyć $rootScope.$applyAsync()
.
Uważaj na to, jeśli aService.foo
jest bardzo często aktualizowany, ponieważ może to znacząco wpłynąć na wydajność. Jeśli wydajność byłaby problemem, możesz ustawić wzorzec obserwatora podobny do innych odpowiedzi tutaj za pomocą setera.