Пишу диплом где решаю одну из задач — реализация анонимного быстрого веб чата. Быстрого во всех смыслах — загрузка, работа приложения, использование (прочь авторизацию). Выбор остановил на связке: 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
Аргументы:
Функция-вычислитель
(обязателен) — кешированный результат этой функции выдаёт memoizeФункция-распознаватель
(опционален) — результат функции является ключом кэша (проверяет уникальность)
_.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