Кэш для фильтров AngularJS с помощью Lo-Dash

в 10:48, , рубрики: AngularJS, javascript, кеширование, метки: ,

AngularJS как быстрый тигр ^.^

Пишу диплом где решаю одну из задач — реализация анонимного быстрого веб чата. Быстрого во всех смыслах — загрузка, работа приложения, использование (прочь авторизацию). Выбор остановил на связке: Node.js фреймворк SocketStream и AngularJS на стороне клиента. В процессе работы столкнулся с проблемой — повторные расчёты производимые фильтрами на одной и той же модели. Детали проблемы и решение под катом.


Уровень подготовки читателя:
AngularJS: средний (создание фильтров)
Lo-Dash: «видел-щупал»

Перейти сразу к решению

Проблема в деталях

У нас есть большой массив, с которым наше приложение постоянно работает манипулируя его элементами. К массиву нужно применять комплексный фильтр, например, сортировка по дате и выделение элементов имеющих определённое свойство. Перенесём эту проблему в прикладную область — упрощённая версия моего чата. Элементами массива являются чаты (комнаты/круги) которые содержат сообщения. Чат имеет такую структуру:

{
   id: 'rE4aA',
   title: 'Тема чата',
   online: 3,
   recent: 0, // Количество новых сообщений
   messages: [] // Сообщения
}

Я хочу выводить на страницу с помощью директивы ngRepeat {N} количество чатов (зависит от размера экрана). И хочу выводить контекстное меню, которое появляется по клику правой кнопки мыши на заголовок любого из чатов и позволяет переместить выбранный чат на место другого. Вот так это выглядит:

Клик правой кнопкой на заголовке чата
Клик правой кнопкой на заголовке чата

Подсветка чата, на место которого метим перемещение
Подсветка чата, на место которого метим перемещение

Такой функционал можно реализовать создав два списка с директивой ngRepeat и применением фильтра. Для чатов фильтр должен уметь сортировать по количеству новых сообщений (свойство recent) и сокращать количество элементов (чатов) до числа {N} которое рассчитывается от размера окна браузера. Для контекстного меню — тот-же фильтр исключая текущий элемент (чат на заголовок которого нажали).

Код фильтра:

angular.module('app')
   .filter('opened', ['$rootScope', function($s){
      return function(o){
         console.log('Применён фильтр «opened»');
         var count = $s.count; // Количество чатов, число {N}
         return _(o) // Оборачиваем массив в Lo-Dash
                   .sortBy('recent') // Сортируем от меньшего к большему
                   .reverse() // Реверсируем (от большего к меньшему)
                   .first(count) // Выделяем первые {N} чатов
                   .value() // Забираем результат
      }
   }]);

Применив этот фильтр к аргументу-массиву переданному каждой директиве ngRepeat увидим, что в консоли сообщение «Применён фильтр «opened» показано дважды. Это значит, что половина ресурсов была потрачена фильтром впустую. Такое удобство как контекстное меню умножило в два раза время рендеринга актуального состояния приложения. А если я продолжу добавлять функционал использующий те же данные с фильтрами, положение ещё сильней усугубится.

Решение проблемы

Решение заключается в создании функции которая возвращает отфильтрованный массив. Эта функция используется вместо исходного массива без использования нативного провайдера фильтров. Функция оборачивается в Lo-Dash свойство memoize, которая реализует функционал кеширования. Ниже я расскажу, как работает memoize и дам пример-реализацию.

Lo-Dash свойство memoize

Аргументы:

  1. Функция-вычислитель (обязателен) — кешированный результат этой функции выдаёт memoize
  2. Функция-распознаватель (опционален) — результат функции является ключом кэша (проверяет уникальность)

_.memoize(fn, [fn]) возвращает функцию, при первом вызове которой производит расчёт, запоминает результат (создает кэш) и возвращает его. При последующих вызовах возвращает кэш. Всё это справедливо для единственного кэш-ключа.

Ключ кэша определяется результатом от функции, которая передаётся вторым аргументом. По умолчанию (если не определён второй аргумент) memoize использует первый аргумент как ключ кэша.

На ярком примере

В конце короткого листинга будет ссылка на демонстрацию, но я предлагаю обратить внимание на комментарии в коде.

Создаём простой контроллер с одним склеенным объектом «form»:

function MyController($scope){
   $scope.form = {
      input: {key:'', val:''}, // Этот объект будем заполнять новыми значениями
      array: [
         {key:'pear', val:'Груша'}, // Предустановка
         {key:'melon', val:'Дыня'},
         {key:'ananas', val:'Ананас'},
         {key:'cherry', val:'Вишня'}
      ],
      order: 'key', // По умолчанию сортируем по свойству key (2 ключа key/val)
      check: false, // Это нужно для теста с доп. ключами кэша (2 ключа — true/false)
      add: function(){ // Метод добавляет новые значения из формы в общий котёл
         this.array.push(angular.copy(this.input));
         this.filtered.cache = {} // Сбрасываем весь кэш
      },
      filtered: _.memoize( // Обращаем внимание
         function(){
            console.log('Фильтровал с параметрами: ' + $scope.form.order + ' и ' + $scope.form.check);
            return _.sortBy($scope.form.array, $scope.form.order)
         },
         function(){ // Генератор кэш-ключей
            // Можно отдавать объект или строку
            return [$scope.form.order, $scope.form.check] // Главное — определить уникальность ключа
         }
      )
   }
}

Немного HTML:

<form name="myform" ng-app ng-controller="MyController">
	<input type="text" required ng-model="form.input.key" placeholder="key">
	<input type="text" required ng-model="form.input.val" placeholder="val">
	<button ng-disabled="!myform.$valid" ng-click="form.add()">Добавить</button><br><br>
	<fieldset>
		<legend>
			Сортировка по свойству:
			<select ng-model="form.order" ng-options="p for p in ['key', 'val']"></select>
		</legend>
		<div ng-repeat="el in form.filtered()">
			{{el.key}} — "{{el.val}}"
		</div><br>
		<label>
			<input type="checkbox" ng-model="form.check"> для проверки кэш-ключа и только
		</label><hr>
		<pre>{{form.filtered()|json}}</pre>
	</fieldset>
</form>

Идём смотреть результат на jsFiddle. Открываем консоль сочетанием Ctrl + Shift + J (актуально для браузера Chrome). Пробуем переключать сортировку и дёргаем флажок. В консоли видим максимум 4 запуска функции-фильтра (на каждое из состояний). Добавив новый элемент в массив — сбросим кэш и снова можем убедиться в правильной работе этого решения.

Благодаря замечательной библиотеки Lo-Dash, и конкретно свойству memoize я серьёзно смог увеличить скорость работы AngularJS приложения. Если бы я применил нативный фильтр, уже с момента запуска приложения, фильтр отработал 8 раз против 1 (решение с memoize).

От сообщества жду конструктивной критики и мыслей о методах «прокачки» нативного фильтра.

P.S.: Благодарю НЛО за приглашение на Хабр.

Автор: sukazavr

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js