Pull to refresh

Дросселирование ввода на AngularJS с помощью debounce

Reading time3 min
Views14K
Original author: Jeremy Likness
Существуют различные сценарии для использования дросселирования (throttling) ввода так, что пересчет значений фильтра будет происходить не каждый раз при изменении значения, а реже. Более подходящий термин — это «устранение дребезга» (debounce), так как в сущности вы ожидаете стабилизации значения на каком-либо постоянном уровне перед вызовом функции, чтобы не вызвать «дребезг» постоянных запросов к серверу. Канонический случай такого рода — это пользователь, вводящий текст в поле ввода для фильтрации списка элементов. Если логика вашего фильтра включает некоторый оверхед (например, фильтрация происходит через REST-ресурс, который выполняет запрос на базе данных бекенда), то вы точно не захотите все время перезапускать и перезагружать результаты запроса в то время, как пользователь пишет текст в поле. Более правильным будет вместо этого подождать, пока он закончит, и уже после этого выполнить запрос один раз.

Простое решение этой проблемы находится тут: jsfiddle.net/nZdgm

Представим, что у вас есть список ($scope.list), который вы публикуете как фильтрованный список ($scope.filteredList) на основе чего-либо содержащего текст из поля $scope.searchText. Ваша форма выглядела бы примерно следующим образом (не обращайте внимание на чекбокс throttle пока что):

<div data-ng-app='App'>
    <div data-ng-controller="MyCtrl">
        <form>
            <label for="searchText">Search Text:</label>
            <input data-ng-model="searchText" name="searchText" />            

            <input type="checkbox" data-ng-model="throttle"> Throttle            

            <label>You typed:</label> <span>{{searchText}}</span>
        </form>
        <ul><li data-ng-repeat="item in filteredList">{{item}}</li></ul>
    </div>
</div>


Типичный сценарий — наблюдать за полем поиска и реагировать мгновенно. Метод фильтрации:

var filterAction = function($scope) {
    if (_.isEmpty($scope.searchText)) {
        $scope.filteredList = $scope.list;
        return;
    }
    var searchText = $scope.searchText.toLowerCase();
    $scope.filteredList = _.filter($scope.list, function(item) {
        return item.indexOf(searchText) !== -1;
    });
};


Контроллер устанавливает $watch примерно следующим образом:

$scope.$watch('searchText', function(){filterAction($scope);});


Такой подход будет запускать фильтрацию каждый раз при вводе в поле. Чтобы устаканить ситуацию, используем встроенную в underscore.js функцию debounce. Функция довольна проста: передайте ей функцию для выполнения и время в миллисекундах. Это задержит реальный вызов функции до тех пор, пока с момента последней попытки ее вызова не пройдет указанное время. Другими словами, при задержке в 1 секунду (которую я использую в этом примере для утрирования эффекта) и непрерывном потоке вызовов функции во время быстрого ввода текста в поле, реальная функция не будет вызвана до тех пор, пока я не прекращу печатать и с этого момента не пройдет 1 секунда.

Может возникнуть искушение сделать простой debounce примерно таким образом:

var filterThrottled = _.debounce(filterAction, 1000);
$scope.$watch('searchText', function(){filterThrottled($scope);});


Однако, тут есть проблема. Такой подход использует таймер, который срабатывает вне цикла $digest, поэтому это в итоге никак не отразится на UI, ведь Angular не знает о случившихся изменениях. Вместо этого вы должны обернуть вызов в $apply:

var filterDelayed = function($scope) {
    $scope.$apply(function(){filterAction($scope);});
};


После этого вы можете установить $watch и отреагировать, как только ввод остановится:

var filterThrottled = _.debounce(filterDelayed, 1000);
$scope.$watch('searchText', function(){filterThrottled($scope);});


Конечно, полноценный пример тут должен включать в себя и throttling, так чтобы можно было увидеть разницу между «мгновенной» фильтрацией и отложенной. Фидл на случай можно посмотреть здесь: jsfiddle.net/nZdgm
Tags:
Hubs:
+14
Comments16

Articles

Change theme settings