Некоторое время назад я писал об использовании Шаблонов нескольких элементов в NativeScript ListView и кратко затронул темы Виртуализация пользовательского интерфейса и Повторное использование представления / компонентов. Похоже, есть некоторые скрытые ловушки, которые вы можете поразить при разработке приложений с помощью ListView, связанных с этим, особенно если вы используете Angular Components в качестве элементов в ListView и сохраняете некоторое состояние в компонентах.
Мы глубоко погрузимся в проблему и покажем некоторые подходы к ее преодолению.
Сценарий
Чтобы продемонстрировать проблему, мы создадим приложение, которое отображает список элементов, и мы хотим иметь возможность выбирать некоторые из них.
Для этого блога мы будем использовать проект, который использует общий код для Интернета и мобильных устройств. Причины:
- Мы можем выделить различия между шаблонами Web и NativeScript.
- Совместное использование кода теперь невероятно просто благодаря angular-cli и @ nativescript / schematics. Узнайте больше об этом в этом замечательном блоге Себастьян Виталек
Вот как приложение будет выглядеть в браузере и симуляторе iOS:

Каждый элемент в списке отображается с помощью ItemComponent - с текущими элементами в качестве параметра @Input. Вот класс компонента:
@Component({ selector: 'app-item', templateUrl: './item.component.html', styleUrls: ['./item.component.css'] }) export class ItemComponent { @Input() item: Item; selected: boolean = false; }
Обратите внимание, что мы сохраняем состояние selected как поле в компоненте. Мы также используем его в нескольких местах в шаблоне:
// Mobile Template (item.component.tns.html)
<StackLayout orientation="horizontal" class="item"
(tap)="selected = !selected">
<Label [text]="item.name"></Label>
<Label class="select-btn"
[class.selected]="selected"
[text]="selected ? 'selected' : 'unselected'">
</Label>
</StackLayout>
// Web Template (item.component.html)
<div class="item">
{{ item.name }}
<span (click)="selected = !selected"
class="select-btn"
[class.selected]="selected">
{{ selected ? 'selected' : 'unselected' }}
</span>
</div>
Здесь находится весь проект вместе с ветками для разных разделов блога:
Использование старого доброго * ngFor
Мы начнем с отображения всех элементов модели в контейнере (также известном как умный) компонент, использующий *ngFor:
<app-item *ngFor="let item of items" [item]="item"></app-item>
Довольно прямолинейно! Это отобразит ItemComponent для каждого элемента в коллекции.
В тестовом проекте сгенерировано 100 элементов, и все работает очень быстро как для Интернета, так и для мобильных устройств.
😈😈😈 Давай попробуем еще раз 😈😈😈
Веб-приложение начинает значительно задерживаться при запуске при 10 КБ элементов. В мобильных пунктах порог намного ниже - около 2К. Это связано с тем, что собственные компоненты, отображаемые IOS / Android, дороже, чем элементы DOM браузера. Если мы сделаем шаблон более сложным, эти цифры уменьшатся.
Но… никто не вносит 2000 пунктов в список, который вы бы сказали. И ты прав. Вы, вероятно, реализуете бесконечную прокрутку с механикой загрузки по требованию. Дело в том, что даже в этом случае вы столкнетесь с проблемами производительности и памяти при прокрутке, поскольку *ngFor будет создавать все больше и больше ItemComponents по мере прокрутки вниз и получения дополнительных данных.
Вот код, чтобы вы могли поиграть с ним сами - просто настройте item.service.ts, чтобы сгенерировать больше элементов: ngFor branch.
Мы можем лучше!
Переключиться на ListView в NS
В NativeScript мы используем собственные элементы управления, которые выполняют виртуализацию пользовательского интерфейса и повторное использование представления / компонентов. Это означает, что будут созданы только элементы пользовательского интерфейса для видимых элементов, и эти элементы пользовательского интерфейса будут переработаны (или повторно использованы) для отображения новых элементов, которые появляются в поле зрения.
Чтобы начать использовать ListView, нам просто нужно изменить шаблон на основе * ngFor, указанный выше, на:
<ListView [items]="items">
<ng-template let-item="item">
<app-item [item]="item"></app-item>
</ng-template>
</ListView>
Большой! Быстрый тест показывает, что теперь мы можем без проблем прокрутить 100K items в мобильном приложении!
Простой счетчик в конструкторе ItemComponent's показывает, что когда-либо создается только 13 экземпляров. Они используются повторно, чтобы отображать все элементы при прокрутке.
Эта проблема
Аккуратный! … либо это? Посмотрим, что произойдет, когда мы начнем выбирать элементы:

Здесь мы видим проблему, которая на самом деле является причиной этого сообщения. Выбираю первые 3 пункта. Когда я прокручиваю вниз, выбираются также пункты 13, 14 и 15. Далее выбираются другие предметы, которых я никогда раньше не видел.
Причина этого в том, что при повторном использовании ItemsComponents состояние, которое находится внутри них, также используется повторно. Когда-либо было создано всего 13 компонентов, поэтому, если вы выберете 3 из них, вы увидите, как они появляются снова и снова при прокрутке.
Если подумать - с этой реализацией вы фактически выбираете компоненты, а не элементы. И между этими двумя коллекциями больше нет отношения 1: 1: есть 100 (или, может быть, 100K😈) элементов и только 13 ItemsComponent экземпляров.
Вот ветка в репо, в которой есть проблема: list-view-state-in-component branch.
Решение
Есть несколько решений, но все они в конечном итоге сводятся к следующему:
Переместите состояние просмотра (поле
selectedв нашем примере) из компонента t и сделайте компонент без состояния.
Мы будем ссылаться на состояние просмотра (из-за отсутствия лучшего термина) для всей информации, которая изначально не была в модели, но все еще используется в шаблонах компонентов и логике приложения. В нашем случае это selected filed. Эта информация также может быть привязана к любому представлению ввода в вашем шаблоне.
Примечание. Один из альтернативных подходов, который приходит на ум, - это попытаться «очистить» компоненты при их повторном использовании. Однако это означает, что вы неизбежно потеряете то состояние, в котором они находились. Просто невозможно хранить 100 предметов в 13 коробках с одним предметом.
Сохранение состояния просмотра в модели
Возможно, самое простое в реализации решение - просто добавить состояние просмотра в элементы модели:
export interface Item {
name: string;
selected?: boolean;
}
Вам нужно будет изменить шаблон компонента, чтобы получить / установить поле selected из item:
<StackLayout orientation="horizontal" class="item"
(tap)="item.selected = !item.selected">
<Label [text]="item.name"></Label>
<Label class="select-btn"
[class.selected]="item.selected"
[text]="item.selected ? 'selected' : 'unselected'">
</Label>
</StackLayout>
Задача решена! Чтобы было понятнее. Мы перешли от ngFor с компонентами с отслеживанием состояния:

в ListView (по-прежнему ngFor в веб-версии) с компонентами без сохранения состояния:

Примечание. В веб-шаблонах по-прежнему используется ngFor. Он отлично работает с версией ItemComponent без сохранения состояния. Вот ветка в репо: ветка list-view-state-in-model
Для простых случаев это подходящее решение, но вы можете не захотеть смешивать свойства состояния представления с моделью. Или может случиться так, что вы получаете объект модели непосредственно из службы и хотите, чтобы они были «чистыми» из дополнительного поля, чтобы вы могли в какой-то момент отправить их обратно.
Прикрепить View-State к элементу
Другой подход заключался бы в том, чтобы иметь состояние представления как отдельный объект состояния представления и «прикреплять» его к объекту модели, когда он используется в пользовательском интерфейсе. Это даст нам некоторое разделение между свойствами модели и состояния представления и простой способ очистки объектов модели при необходимости.
Чтобы упростить задачу, я создал декоратор TypeScript, который сделает всю работу за меня. Вот как это происходит:
- Мы украшаем специальное свойство состояния представления в компоненте (назовем его для краткости
vs) с помощью нашего специального декоратора:@attachViewState. - Мы даем декоратору фабричную функцию для создания объекта состояния просмотра по умолчанию для элементов. Он будет использовать его всякий раз, когда ему нужно создать объект состояния просмотра для элемента.
- Мы даем декоратору имя фактического свойства модели в компоненте. Обычно это свойство
@Input- в нашем случае «элемент». - Декоратор создаст (используя фабрику) и «прикрепит» объект состояния просмотра к каждому элементу, переданному компоненту («прикрепить» - это причудливый способ сказать, что он установит свойство
"__vs"для элемента). - Декоратор также изменит геттер и сеттер для свойства
vs, чтобы они получили доступ к объекту состояния представления, который находится внутри элемента. Это упростит использование состояния просмотра внутри шаблона компонента.
Звучит сложно? На самом деле пользоваться им довольно просто:
interface ItemViewState {
selected?: boolean;
}
const ItemViewStateFactory = () => { return { selected: false } };
@Component({ ... })
export class ItemComponent {
@attachViewState<ItemViewState>("item", ItemViewStateFactory)
vs: ItemViewState;
@Input() item: Item;
}
И в шаблоне мы просто используем vs для свойств состояния просмотра и item для свойств данных:
<StackLayout orientation="horizontal" class="item"
(tap)="vs.selected = !vs.selected">
<Label [text]="item.name"></Label>
<Label class="select-btn"
[class.selected]="vs.selected"
[text]="vs.selected ? 'selected' : 'unselected'">
</Label>
</StackLayout>
Вот также код декоратора @attachViewState (T - это тип объекта состояния просмотра). Существуют также вспомогательные методы getViewState и cleanViewState для получения и очистки объекта состояния представления из модели.
Опять же, код здесь: ветвь list-view-state-in-model-decorator
Примечание: есть и другие тактики. Например:
- поддерживать полностью разделенный список объектов состояния просмотра в вашем компоненте контейнера и передавать их как входные данные шаблона
- «Оберните» элемент модели в элементы модели представления, используя композицию, таким образом, полностью сохраняя элементы модели нетронутыми.
Бонус (случай для компонентов без сохранения состояния)
Стоит отметить, что это решение безупречно работает в веб-версии нашего приложения, где *ngFor все еще используется. Фактически, во многих случаях наличие компонентов без сохранения состояния фактически приведет к лучшей архитектуре приложения.
Вот пример. Рассмотрим следующую функцию в нашем приложении: мы должны собрать все выбранные элементы и отобразить их в другом представлении (или просто alert их пока 😃).
Если «выбранная» информация находится внутри компонентов, нам придется либо:
- Используйте
@ViewChildrenдля запроса компонентов, чтобы выяснить, какие из них выбраны. 🤮Eew! 🤮 - Предоставьте какое-то событие для уведомления всякий раз, когда элемент выбран, и обработайте его в компоненте контейнера. Это означает, что мы будем хранить «выбранную» информацию в двух разных местах (один раз в
ItemComponentи один раз в компоненте-контейнере). Eeeew! 🤮🤮
С другой стороны, если у вас есть состояние без состояния ItemComponent и вы храните состояние отдельно, вам будет легче работать с данными. Вот как выглядит код, если вы используете подход «декоратора» сверху (мы используем метод getViewState из вспомогательной утилиты для получения состояния просмотра):
// In container-component template (home.component.html):
...
<button (click)="checkout()">checkout</button>
...
// In container-component code (home.component.ts):
...
checkout() {
const result = this.items
.filter(item => {
const vs = getViewState<ItemViewState>(item);
return vs && vs.selected;
})
.map(item => item.name)
.join("\n");
alert("Selected items:\n" + result);
}
Код финального проекта: master branch
Резюме
Вот основные выводы:
- При переключении с
*ngForнаListViewимейте в виду, что он перерабатывает компоненты вашего шаблона. Любое состояние внутри них (все не@Inputсвойства, которые не связаны в шаблоне) переживет повторную переработку и, вероятно, вызовет нежелательное поведение. - Рассмотрите возможность использования компонентов без сохранения состояния (также известных как презентация). Это избавит вас от проблем в 1., поскольку все состояния будут передаваться как входные. Он также соответствует руководствам Умные компоненты против презентационных компонентов и приведет к лучшей архитектуре вашего приложения.
- Бонус: Совместное использование кода между Интернетом и мобильным телефоном с помощью NativeScript теперь действительно просто. Не совсем по теме ... но я взволнован и решил поделиться 😃😃😃