Фильтр Angular работает, но приводит к тому, что итерации в 10 $перевариваются "
Я получаю данные с моего заднего сервера, структурированного следующим образом:
{
name : "Mc Feast",
owner : "Mc Donalds"
},
{
name : "Royale with cheese",
owner : "Mc Donalds"
},
{
name : "Whopper",
owner : "Burger King"
}
По моему мнению, я хотел бы "инвертировать" список. То есть Я хочу перечислить каждого владельца, и для этого владельца список всех гамбургеров. Я могу добиться этого, используя функцию underscorejs groupBy
в фильтре, которую я затем использую с директивой ng-repeat
:
JS:
app.filter("ownerGrouping", function() {
return function(collection) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}
});
HTML:
<li ng-repeat="(owner, hamburgerList) in hamburgers | ownerGrouping">
{{owner}}:
<ul>
<li ng-repeat="burger in hamburgerList | orderBy : 'name'">{{burger.name}}</li>
</ul>
</li>
Это работает так, как ожидалось, но я получаю огромную трассировку стека ошибок, когда список отображается с сообщением об ошибке "Достигнутые итерации 10 $digest". Мне нелегко видеть, как мой код создает бесконечный цикл, который подразумевается в этом сообщении. Кто-нибудь знает почему?
Вот ссылка на plunk с кодом: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview
Ответы
Ответ 1
Это происходит потому, что _.groupBy
возвращает коллекцию новых объектов каждый раз, когда она запускается. Angular ngRepeat
не понимает, что эти объекты равны, потому что ngRepeat
отслеживает их по идентификатору. Новый объект приводит к новой идентичности. Это означает, что Angular считает, что с момента последней проверки что-то изменилось, а это означает, что Angular должен запустить другую проверку (aka digest). Следующий дайджест заканчивается тем, что появляется еще один новый набор объектов, и поэтому запускается другой дайджест. Повторяется до Angular.
Один простой способ избавиться от ошибки - убедиться, что ваш фильтр возвращает один и тот же набор объектов каждый раз (если, конечно, он не изменился). Вы можете сделать это очень легко с подчеркиванием, используя _.memoize
. Просто оберните функцию фильтра в memoize:
app.filter("ownerGrouping", function() {
return _.memoize(function(collection, field) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}, function resolver(collection, field) {
return collection.length + field;
})
});
Функция распознавания требуется, если вы планируете использовать разные значения полей для ваших фильтров. В приведенном выше примере используется длина массива. Лучше всего сократить сбор до уникальной строки хеша md5.
Смотрите здесь плункерную вилку. Memoize запомнит результат конкретного ввода и вернет тот же объект, если вход такой же, как и раньше. Если значения часто меняются, тогда вы должны проверить, не удаляет ли _.memoize
старые результаты, чтобы избежать утечки памяти со временем.
Следя немного дальше, я вижу, что ngRepeat
поддерживает расширенный синтаксис ... track by EXPRESSION
, что может быть полезно как-то, позволяя вам рассказать Angular посмотреть на owner
в ресторанах, а не на личность объекты. Это было бы альтернативой трюку memoization выше, хотя мне не удалось проверить его в плункере (возможно, была реализована старая версия Angular с track by
?).
Ответ 2
Хорошо, думаю, я понял это. Начните с ознакомления с исходным кодом для ngRepeat. Строка уведомления 199: Здесь мы настраиваем часы на массиве/объекте, который мы повторяем, так что, если он или его элементы изменят цикл дайджеста, будет запущен:
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
Теперь нам нужно найти определение $watchCollection
, которое начинается в строке 360 rootScope.js. Эта функция передается в нашем выражении массива или объекта, которое в нашем случае hamburgers | ownerGrouping
. В строке 365 строковое выражение превращается в функцию с помощью службы $parse
, функция, которая будет вызываться позже, и каждый раз, когда выполняется этот наблюдатель:
var objGetter = $parse(obj);
Эта новая функция, которая будет оценивать наш фильтр и получать результирующий массив, вызывается только несколькими строками вниз:
newValue = objGetter(self);
Итак, newValue
содержит результат наших отфильтрованных данных после применения groupBy.
Затем прокрутите вниз до строки 408 и посмотрите на этот код:
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
При первом запуске oldValue представляет собой пустой массив (настроенный выше как "internalArray" ), поэтому будет обнаружено изменение. Тем не менее, каждый из его элементов будет установлен в соответствующий элемент newValue, так что мы ожидаем, что в следующий раз, когда он будет запущен, все должно совпадать, и никакие изменения не будут обнаружены. Поэтому, когда все работает нормально, этот код будет запускаться дважды. Однажды для настройки, которая обнаруживает изменение из исходного состояния null, а затем еще раз, потому что обнаруженное изменение заставляет запускать новый цикл дайджеста. В нормальном случае изменения не будут обнаружены во время этого 2-го запуска, потому что в этой точке (oldValue[i] !== newValue[i])
будет false для всех i. Вот почему в вашем рабочем примере вы видели 2 выхода console.log.
Но в вашем случае сбоя ваш код фильтра генерирует новый массив с новыми elments каждый раз, когда он запускается. В то время как эти новые элементы массива имеют то же значение, что и старые элементы массива (это идеальная копия), они не являются одинаковыми фактическими элементами. То есть они ссылаются на разные объекты в памяти, которые просто имеют одинаковые свойства и значения. Следовательно, в вашем случае oldValue[i] !== newValue[i]
всегда будет истинным, по той же причине, что, например, {x: 1} !== {x: 1}
всегда истинно. И изменение всегда будет обнаружено.
Таким образом, основная проблема заключается в том, что ваш фильтр создает новую копию массива каждый раз, когда он запускается, состоящий из новых элементов, которые являются копиями исходных элементов массива. Таким образом, настройка наблюдателя ngRepeat просто застревает в том, что по существу является бесконечным рекурсивным циклом, всегда обнаруживая изменения и запуская новый цикл дайджеста.
Вот более простая версия вашего кода, которая воссоздает ту же проблему: http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview
Проблема исчезает, если фильтр перестает создавать новый массив каждый раз, когда он запускается.
Ответ 3
Новое для AngularJS 1.2 является опцией "track-by" для директивы ng-repeat. Вы можете использовать его, чтобы помочь Angular признать, что разные экземпляры объектов должны действительно считаться одним и тем же объектом.
ng-repeat="student in students track by student.id"
Это поможет unconfuse Angular в таких случаях, как ваша, где вы используете Underscore, чтобы выполнять нарезку и нарезку в тяжелом весе, создавая новые объекты, а не просто фильтруя их.
Ответ 4
Спасибо за решение memoize, он отлично работает.
Однако _. memoize использует первый переданный параметр как ключ по умолчанию для своего кеша. Это не может быть удобно, особенно если первый параметр всегда будет одной и той же ссылкой. Надеемся, что это поведение настраивается с помощью параметра resolver
.
В приведенном ниже примере первый параметр всегда будет одним и тем же массивом, а второй - строкой, представляющей в каком поле ее следует сгруппировать:
return _.memoize(function(collection, field) {
return _.groupBy(collection, field);
}, function resolver(collection, field) {
return collection.length + field;
});
Ответ 5
Измените краткость, но попробуйте ng-init="thing = (array | fn:arg)"
и используйте thing
в своем ng-repeat
. Работает для меня, но это широкая проблема.
Ответ 6
Я не уверен, почему эта ошибка возникает, но логически функция фильтра вызывается для каждого элемента массива.
В вашем случае функция фильтра, которую вы создали, возвращает функцию, которую следует вызывать только при обновлении массива, а не для каждого элемента массива. Результат, возвращаемый функцией, может быть затем ограничен html.
Я разветкил плункер и создал свою собственную реализацию здесь http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn
Он не использует фильтр. Основная идея состоит в том, чтобы вызвать groupBy
в начале и всякий раз, когда добавлен элемент
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
$scope.addBurger = function() {
hamburgers.push({
name : "Mc Fish",
owner :"Mc Donalds"
});
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
}
Ответ 7
Для чего стоит добавить еще один пример и решение, у меня был простой фильтр:
.filter('paragraphs', function () {
return function (text) {
return text.split(/\n\n/g);
}
})
с:
<p ng-repeat="p in (description | paragraphs)">{{ p }}</p>
что вызвало описанную бесконечную рекурсию в $digest
. Легко фиксируется с помощью
<p ng-repeat="(i, p) in (description | paragraphs) track by i">{{ p }}</p>
Это также необходимо, так как ngRepeat
парадоксально не любит повторителей, т.е. "foo\n\nfoo"
приведет к ошибке из-за двух идентичных абзацев. Это решение может быть неприемлемым, если содержимое абзацев действительно меняется, и важно, чтобы они продолжали усваиваться, но в моем случае это не проблема.