Узнайте, как правильно использовать директиву * ngFor для оптимизации производительности вашего приложения Angular

*ngFor - одна из самых популярных директив в Angular, однако при неправильном использовании она может снизить производительность вашего приложения. В этом посте мы узнаем, как правильно использовать директиву *ngFor, чтобы предотвратить замедление работы нашего приложения.

Совет в духе оптимизации производительности: оптимизируйте код, создавая, совместно используя и используя повторно используемые компоненты Angular с Bit.

*ngFor Директива

Эта директива используется для рендеринга массива повторяемых объектов (или просто массивов). Это то же самое, что ng-repeat в AngularJS.

<ul>
    <li ng-repeat="user in users">
        Name: {{user.name}}
        Address: {{user.address}}
        Phone Number: {{user.phone}}
    </li>
</ul>

Оба они используются для создания повторяющегося контента, такого как список клиентов, элементы раскрывающегося списка и т. Д.

*ngFor - структурная директива. Он используется для создания и управления DOM путем добавления и удаления элементов DOM.

@component({
    template: `
        <ul>
            <li *ngFor="let user of users">
                Name: {{user.name}}
                Address: {{user.address}}
                Phone Number: {{user.phone}}
            </li>
        </ul>
    `
})
class NgForEx {
    users: Array<User> = [
        {
            name: "Stones",
            address: "Nyanya, Abuja",
            phone: 08163393726
        },
        {
            name: "Marvel",
            address: "Kree Planet",
            phone: 081234638745
        }
    ] 
}
interface User {
    name: string;
    address: string;
    phone: number;
}

Результат будет выглядеть так:

<ul>
    <li>
        Name: Stones
        Address: Nyanya, Abuja
        Phone Number: 08163393726
    </li>
    <li>
        Name: Marvel
        Address: Kree Planet
        Phone Number: 081234638745
    </li>
</ul>

У нас есть Пользователь с именем, адресом и телефоном. Мы инициализируем свойство массива users в классе NgForEx. Мы использовали директиву ngFor для перебора массива users и заполнения каждой DOM шаблонными выражениями: {{user.name}}, {{user.address}} и {{user.phone}}.

Мы можем расширить приложение, чтобы мы могли добавлять больше пользователей в массив пользователей.

@component({
    template: `
        <div>
            <div>
                <input type="text" placeholder="Enter Name" [(ngModel)]="name" />
                <input type="number" placeholder="Enter Age" [(ngModel)]="age" />
                <input type="number" placeholder="Enter Phone" [(ngModel)]="phone" />
                <button (click)="addUser()">Add User</button>
            </div>
            <ul>
                <li *ngFor="let user of users">
                    Name: {{user.name}}
                    Address: {{user.address}}
                    Phone Number: {{user.phone}}
                </li>
            </ul>
        </div>
    `
})
class NgForEx {
    name: string = ""
    address: string
    phone: number
    users: Array<User> = [
        {
            name: "Stones",
            address: "Nyanya, Abuja",
            phone: 08163393726
        }.
        {
            name: "Marvel",
            address: "Kree Planet",
            phone: 081234638745
        }
    ] 
    addUser() {
        this.users = this.users.concat([{
            name: this.name,
            age: this.age,
            phone: this.phone
        }])
        this.clearDetails()
    }
    clearDetails() {
        this.name = ""
        this.age = null
        this.phone = null
    }
}

Когда мы заполняем форму и нажимаем кнопку Add User, значения входных данных агрегируются и добавляются в массив пользователей. Компонент выполняет повторную визуализацию, чтобы обновить представление новой записи в массиве пользователей.

Отрисовка массива в ngFor вызывает чрезмерные манипуляции с DOM.

Наш приведенный выше пример слишком прост, чтобы показать какие-либо замедления в работе приложения. Представьте, что вы перебираете более 10000 объектов в массиве.

@component({
    template: `
        <div>
            <div>
                <input type="text" placeholder="Enter Name" [(ngModel)]="name" />
                <input type="number" placeholder="Enter Age" [(ngModel)]="age" />
                <input type="number" placeholder="Enter Phone" [(ngModel)]="phone" />
                <button (click)="addUser()">Add User</button>
            </div>
            <ul>
                <li *ngFor="let user of users">
                    Name: {{user.name}}
                    Address: {{user.address}}
                    Phone Number: {{user.phone}}
                </li>
            </ul>
        </div>
    `
})
class NgForEx {
    name: string = ""
    age: number
    phone: number
    users: Array<User> = [
        {
            id: 0,
            name: "Stones",
            address: "Nyanya, Abuja",
            phone: 08163393726
        }.
        {
            id: 1,
            name: "Marvel",
            address: "Kree Planet",
            phone: 081234638745
        }
    ] 
    id: number = 1
    constructor() {
        for(var i = 0; i < 100000; i++) {
            this.users.push({
                id: this.id++,
                name: "Unknown",
                address: "Abuja",
                phone: 0384793 + this.id++
            })
        }
    }
    addUser() {
        this.users = this.users.concat([{
            id: this.id++,
            name: this.name,
            age: this.age,
            phone: this.phone
        }])
        this.clearDetails()
    }
}

Здесь мы заполняем массив пользователей 100000 пользователей. Когда мы нажимаем кнопку Add User, чтобы добавить пользователя, директива ngFor уничтожает <li>...</li> модели DOM, перебирает массив пользователей и генерирует новую коллекцию элементов разметки вывода <li>...</li> с массивом пользователей, который нужно отобразить.

Не понимаете? Не беспокойтесь, позвольте мне объяснить с помощью визуальных эффектов.

Допустим, у нас есть это:

users = [
    {
        name: nnamdi
    },
    {
        name:  chidume
    }]
<ul>
    <li>
        nnamdi
    </li>
    <li>
        chidume
    </li>
</ul>

Теперь, когда мы добавляем нового пользователя в массив пользователей, например

{
    "name": "philip"
}

Весь DOM

<ul>
    <li>
        nnamdi
    </li>
    <li>
        chidume
    </li>
</ul>

будет уничтожен

<ul>
</ul>

и новый DOM будет сгенерирован из массива пользователей:

users = [
    {
        name: "nnamdi"
    },
    {
        name:  "chidume"
    },
    {
        name: "philip"
    }]
<ul>
    <li>
        nnamdi
    </li>
    <li>
        chidume
    </li>
    <li>
        philip
    </li>
</ul>

С какой проблемой мы здесь столкнемся?

Это увеличит время, необходимое браузеру для отображения изменений в DOM. Полное разрушение и создание узлов DOM, особенно в больших масштабах, серьезно снизит скорость и производительность нашего приложения.

Мутация и различия

Виновником этой неэффективной манипуляции с DOM является не директива the*ngFor, а наше понимание и использование концепции изменчивости в Angular.

Angular использует оператор проверки равенства ===, чтобы определить, когда ввод (ы) в директивы изменяется. Когда он видит изменение, он запускает повторный рендеринг или компакт-диск с директивой.

Примечание. Компоненты - это директивы, но это директивы с целью.

Оператор === проверяет наличие изменений ссылок в проверяемых объектах. Что такое изменения ссылок? Ссылка в сложной структуре данных в JS - это адрес памяти, где объект хранится в куче.

const a = [1, 2, 3, 4, 5] // stored at #0x0987654321
const b = [1, 2, 3, 4, 5] // stored at #0x0987654326

Массивы - это пример сложной структуры данных. Массив a хранится в 0x0987654321, а массив b хранится в 0x0987654326. Оценка идентичности двух массивов с помощью === вернет false, потому что они указывают на разные адреса памяти.

log(a === b)  // false

Хотя они содержат одинаковые значения, он вернул false, потому что не сравнивает их значения, а только сравнивает их адреса в памяти.

Если мы сделаем b, указывающим на a, вышеуказанная проверка на равенство зафиксирует истину.

const a = [1, 2, 3, 4, 5] // stored at #0x0987654321
const b = [1, 2, 3, 4, 5] // stored at #0x0987654326
b = a // Now, b is stored at #0x0987654321
log(b === a) // true

Использование оператора = в сложной структуре данных просто изменяет его адресный указатель на значение адреса памяти LHS. Выполнение b = a просто указывает b на адрес памяти a #0x0987654321. Журнал возвращает истину, потому что === проверяет адрес памяти LHS и структуры данных RHS.

Что такое мутация? Это изменение структуры / информации набора данных. У нас такой a:

const a = [1, 2, 3, 4, 5] // stored at #0x0987654321

Если мы добавим в него данные, его структура изменится.

a = [1, 2, 3, 4, 5, 6] // stored at #0x0987654321
// OR
a = [1, 2, 3, 4, 5, "nnamdi"] // stored at #0x0987654321

Теперь мы бы сказали, что a был видоизменен.

Поскольку Angular использует === для проверки изменений в структурах данных. Поэтому нам рекомендуется использовать немутантные методы при манипулировании нашими входными привязками, потому что, если мы используем изменяющие методы, Angular не получит никаких изменений для запуска CD, хотя внутренние значения изменились.

Немутантные методы всегда возвращают новую структуру данных, адрес памяти которой отличается от исходной родительской структуры данных.

Теперь *ngFor использует Differs, чтобы знать, когда выполнять итерацию по коллекции, чтобы узнать, какие элементы коллекции нужно отобразить. У Differs есть методы, которые сообщают нам, что:

  • были добавлены
  • которые удалили
  • которые изменили свою позицию

Если упростить исходный код ngFor, это будет выглядеть так:

@Directive({
    selector:'[ngForOf]'
})
class ngForOf implements OnChanges, DoCheck {
    differ: any
    items: any
    set ngForOf(items) {
          this.items = items
          this.differ = this.differs.find(items).create()
    }
    ngDoCheck() {
          const changes = this.differ.diff(this.its)
          if(changes) {
              changes.forEachAddedItem((change) => {
                  this.viewContainer.createEmbeddView(...)
              })
          }
    }
}

В ngDoCheck мы используем метод diff для обнаружения изменений в списке items. Если есть изменения, мы перебираем добавленные элементы и создаем представления из добавленных элементов.

Как Differs обнаруживает изменения в списке массивов? Они используют одну и ту же проверку на равенство ===. Итак, если у нас есть такой массив:

let array = [
    {
        id: 0
    },
    {
        id: 1
    },
    {
        id: 2
    }
]

Differs записывает ссылки списков и позиции:

let array = [
    {
        id: 0
    }, // @ #0x000001
    {
        id: 1
    }, // @ #0x000002
    {
        id: 2
    } // @ #0x000003
]

Когда запись добавляется в список, array.push({id:3}):

let array = [
    {
        id: 0
    }, // @ #0x000001
    {
        id: 1
    }, // @ #0x000002
    {
        id: 2
    }, // @ #0x000003
    {
        id: 3
    } // @ #0x000004
]

Differs записывает:

{
    id: 3
} // @ #0x000004

как добавленный элемент.

Поэтому, когда мы вызываем changes.forEachAddedItem(()=> void), указанный выше элемент

{
    id: 3
} // @ #0x000004

будет возвращен

Теперь, если мы выполним операцию, которая изменяет список массивов и возвращает новый набор списков.

let array = [
    {
        id: 0
    }, // @ #0x000001
    {
        id: 1
    }, // @ #0x000002
    {
        id: 2
    }, // @ #0x000003
]
array = array.concat([
    {
        id: 3
    }
])

массив будет

array = [
    {
        id: 0
    }, // @ #0x000011
    {
        id: 1
    }, // @ #0x000012
    {
        id: 2
    }, // @ #0x000013
    {
        id: 3
    } // @ #0x000014
]

Вы видите, что возвращаются новые ссылки. Differs сравню пред. array с новым массивом и посмотрите, как изменится ссылка. Затем указанный выше массив будет возвращен как добавленные элементы.

Итак, вы видите проблему? Из-за мутации и запуска Angular CD мы всегда возвращаем новую ссылку в наших компонентах, и поэтому Differs возвращает новые изменения во входных элементах ngFor. В этом процессе ngFor будет продолжать отображать неизмененные элементы, поскольку они были добавлены из-за изменения ссылки.

Как решить эту проблему?

Использование trackBy

Differs предоставляет функцию, с помощью которой мы можем использовать наш собственный механизм обнаружения изменений в нашем списке. Differs будет использовать функцию в trackBy, чтобы определить, как обнаруживать изменения в элементах в списке.

Как у нас есть массив:

array = [
    {
        id: 0,
        name: "david"
    }, // @ #0x0001
    {
        id: 1,
        name: "philip"
    } // @ #0x0002
]
trackBy: (item) => item.id

Differs запишут этот набор своими id. Когда мы добавляем еще один, либо возвращая новый массив, либо обновляя текущий массив.

array = array.concat([
    {
        id: 2,
        name: "nnamdi"
    }
])
array = [
    {
        id: 0,
        name: "david"
    }, // @ #0x0011
    {
        id: 1,
        name: "philip"
    }, // @ #0x0012
    {
        id: 2,
        name: "nnamdi"
    } // @ #0x0013
]
trackBy: (item) => item.id

Смотрите новые ссылки в массиве, но Differs не будет их использовать, он будет использовать функцию trackBy: (item) => item.id и обнаруживать изменения по идентификатору элементов. Итак, теперь Differs прочитает следующие изменения:

{
    id: 2,
    name: "nnamdi"
} // @ #0x0013

:) Вы видите, что он вернул вышеуказанное, потому что id не входит в число пред. записанный список.

То же самое происходит, когда массив обновляется без изменения ссылки.

array = [
    {
        id: 0,
        name: "david"
    }, // @ #0x0001
    {
        id: 1,
        name: "philip"
    } // @ #0x0002
]
trackBy: (item) => item.id
array.push({
    id: 2,
    name: "nnamdi"    
})
array = [
    {
        id: 0,
        name: "david"
    }, // @ #0x0001
    {
        id: 1,
        name: "philip"
    }, // @ #0x0002
    {
        id: 2,
        name: "nnamdi"
    } // @ #0x0003
]

Будет возвращен идентификатор с 2:

{
    id: 2,
    name: "nnamdi"
} // @ #0x0003

Теперь давайте перепишем наш NgForEx пример, чтобы использовать параметр trackBy:

@component({
    template: `
        <div>
            <div>
                <input type="text" placeholder="Enter Name" [(ngModel)]="name" />
                <input type="number" placeholder="Enter Age" [(ngModel)]="age" />
                <input type="number" placeholder="Enter Phone" [(ngModel)]="phone" />
                <button (click)="addUser()">Add User</button>
            </div>
            <ul>
                <li *ngFor="let user of users; trackBy: trackById">
                    Name: {{user.name}}
                    Address: {{user.address}}
                    Phone Number: {{user.phone}}
                </li>
            </ul>
        </div>
    `
})
class NgForEx {
    name: string = ""
    age: number
    phone: number
    users: Array<User> = [
        {
            id: 0,
            name: "Stones",
            address: "Nyanya, Abuja",
            phone: 08163393726
        }.
        {
            id: 1,
            name: "Marvel",
            address: "Kree Planet",
            phone: 081234638745
        }
    ] 
    id: number = 1
    constructor() {
        for(var i = 0; i < 100000; i++) {
            this.users.push({
                id: this.id++,
                name: "Unknown",
                address: "Abuja",
                phone: 0384793 + this.id++
            })
        }
    }
    addUser() {
        this.users = this.users.concat([{
            id: this.id++,
            name: this.name,
            age: this.age,
            phone: this.phone
        }])
        this.clearDetails()
    }
    trackById(index: number, item: User) {
        return item.id
    }
}

Теперь Angular будет использовать значение, возвращаемое функцией trackById, для отслеживания идентичности элементов.

Заключение

В этом посте мы увидели проблему, которую создает ngFor при повторном рендеринге больших списков из-за неизменности. Кроме того, мы узнали, как использовать параметр функции trackBy в Differs, чтобы решить эту проблему, тем самым уменьшив наполовину тяжелое создание и уничтожение DOM.

Если у вас есть какие-либо вопросы или вы нашли что-то, что я должен добавить, исправить или удалить - не стесняйтесь комментировать, писать мне по электронной почте или в DM!

Спасибо !!!