+3

Tăng performance cho ng-repeat với "track by"

Một trong những thứ đầu tiên mà cơ bản nhất khi học Angular đó là ngRepeat:

<ul class="tasks">
    <li ng-repeat="task in tasks" ng-class="{done: task.done}">
        {{task.id}}: {{task.title}}
    </li>
</ul>

Problem

Giờ giả định ta có một chức năng refresh lại data, mỗi khi click vào, ta thực hiện GET danh sách task và gán lại cho biến tasks trong controller: $scope.tasks = newTasksFromTheServer; Đoạn code trên sẽ buộc ngRepeat phải remove hết các li element hiện đang có và tạo lại chúng lần nữa, việc này sẽ rất tốn performance thao tác trên DOM nếu như có rất rất nhiều li hoặc cấu trúc template của li phức tạp như chứa nhiều filter, format, v.v...khiến cho đôi khi ta có cảm giác giật lag trên màn hình.

// View
<div ng-controller="TasksCtrl">
    <button ng-click="refresh()">Refresh Tasks</button> Click this to feel the delay, then change the HTML on the left to see the no-delay version that uses "track by"
    
    <ul class="tasks">        
        <li ng-repeat="task in tasks" task="task" class="task"></li>
        <!--<li ng-repeat="task in tasks track by task.id" task="task" class="task"></li>-->
    </ul>
</div>
// Controller
angular.module('myApp', [])
.controller('TasksCtrl', function($scope) {
    var date = new Date().toISOString();
    $scope.tasks = [];
    for (var i = 1; i < 13001; i++) {
        $scope.tasks.push({
            id: i,
            title: 'Task ' + i, 
            date: date
        });
    }
    
    $scope.refresh = function() {
        console.log("refreshing")
        $scope.tasks = angular.copy($scope.tasks);
    };
})
.directive('task', function() {
    return {
        scope: {
            task: '='
        },
        template: '{{ task.title }} <span class="time">({{ prettyTaskDate }})</span>',
        link: function(scope) {
            console.log('rendering', scope.task.id);
      
            scope.$watch('task.date', function() {
                scope.prettyTaskDate = moment(scope.task.date).fromNow();
            });
        }
    };
});

Câu hỏi đặt ra là tại sao lại như vậy ? Thực tế, ngRepeat thêm property $$hashKey vào mỗi task để tracking task đó. Nếu ta thay thế tasks ban đầu với một tasks object mới từ server, kể cả nội dung tasks mới đó chả khác gì tasks hiện tại, thì ngRepeat cũng vẫn sẽ không nhận ra bởi tasks mới sẽ không chứa $$hashKey, từ đó buộc phải render lại.

Giải pháp tình thế

Giải pháp đầu tiên có thể nghĩ đến đó là, thay vì replace toàn bộ tasks bằng tasks mới, ta có thể giữ nguyên $$hashKey bằng cách loop qua tasks và chỉ cập nhật property. Nghe có vẻ khá là cơ bắp nhỉ ? Chưa kể làm như vậy khá là khó đọc và khó để maintain.

$scope.refresh = function() {
   console.log("refreshing")
   var newTasks = $scope.tasks;
   $scope.tasks.forEach(function(t){
      var newTask = newTasks.find(function(nT){return nT.id === t.id});
      t.a = newTask.a;
      t.b = newTask.b;
      ...
   })
};

Track by

Từ phiên bản Angular 1.2, một sự bổ sung được thêm vào cú pháp của ngRepeat đó là: mệnh đề track by. Nó cho phép ta tự chỉ định key để ngRepeat track objects thay vì key tự gen ra ($$hashKey). Thay đoạn trên bằng:

ng-repeat="task in tasks track by task.id"

như vậy ngRepeat sẽ biết tasks ban đầu và updated tasks là một và sẽ ko tạo lại DOM mà tái sử dụng chúng.

KẾT LUẬN

Tài liệu tham khảo

  1. https://www.codelord.net/2014/04/15/improving-ng-repeat-performance-with-track-by/

All rights reserved

Bình luận

Đăng nhập để bình luận
Avatar
@chungminhtu
thg 10 23, 2018 4:20 SA

Đối với Angular từ bản 2 trở đi (Angular 2,4,5,6) thì chúng ta phải viết riêng hàm Track By ở file typescript. Cách viết như dưới đây:

@Page({
    template: `
        <ul>
            <li *ngFor="#post of posts;trackBy:identify"> <!-- Chỗ này gọi hàm TrackBy Identity ở trong code ts -->
              {{post.data}}
            </li>
        </ul>
    `
})
export class HomeworkAddStudentsPage {
    posts:Array<{id:number,data:string}>;   

    constructor() {
        this.posts = [  {id:1,data:'post with id 1'},
                        {id:2,data:'post with id 2'} ];
    }

    identify(index,item){
      //Chỗ này làm logic gì để track by cũng được. Chỉ cần trả về giá trị sau khi so sánh là đc. Ở đây mình chỉ đơn giản là so sánh Id.
      return post.id 
     }

}

Tham khảo thêm bài viết này nữa nhé:

https://netbasal.com/angular-2-improve-performance-with-trackby-cc147b5104e5

Avatar
@duyth1993
thg 10 23, 2018 4:21 SA

Thank you anh

Avatar
+3
Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí