Jak opóźnić wyszukiwanie natychmiastowe AngularJS?


147

Mam problem z wydajnością, którego nie mogę rozwiązać. Mam wyszukiwanie natychmiastowe, ale jest trochę opóźnione, ponieważ zaczyna szukać na każdymkeyup() .

JS:

var App = angular.module('App', []);

App.controller('DisplayController', function($scope, $http) {
$http.get('data.json').then(function(result){
    $scope.entries = result.data;
});
});

HTML:

<input id="searchText" type="search" placeholder="live search..." ng-model="searchText" />
<div class="entry" ng-repeat="entry in entries | filter:searchText">
<span>{{entry.content}}</span>
</div>

Dane JSON nie są nawet tak duże, tylko 300KB, myślę, że to, co muszę zrobić, to ustawić opóźnienie około 1 sekundy w wyszukiwaniu, aby poczekać, aż użytkownik skończy pisać, zamiast wykonywać akcję po każdym naciśnięciu klawisza. AngularJS robi to wewnętrznie i po przeczytaniu dokumentów i innych tematów na ten temat nie mogłem znaleźć konkretnej odpowiedzi.

Byłbym wdzięczny za wszelkie wskazówki, jak mogę opóźnić wyszukiwanie natychmiastowe.


1
Otrzymujesz cały plik json w aplikacji init ... a następnie filtr wyszukiwania nie pobiera danych po raz drugi podczas pisania ... filtruje już istniejący model. Mam rację?
Maksym

Czy odpowiedź poniżej zadziałała? Jeśli tak, zaakceptuj odpowiedź. Jeśli nie, daj mi znać, a wyjaśnię dalej.
Jason Aden

Hej Jason, dzięki za odpowiedź. Próbowałem pobawić się Twoim kodem, ale bez powodzenia, wyszukiwanie całkowicie przestało działać.
dyskusja mózgowa

Nieważne, to moja wina, że ​​coś przeoczyłem. Twoje rozwiązanie rzeczywiście działa. Dziękuję :)
braincomb

Spójrz na tę odpowiedź tutaj, która zawiera dyrektywę, która pozwala na opóźnienie zmiany ng-change: stackoverflow.com/questions/21121460/…
Doug

Odpowiedzi:


121

(Zobacz odpowiedź poniżej dla rozwiązania Angular 1.3.)

Problem polega na tym, że wyszukiwanie będzie wykonywane za każdym razem, gdy model się zmieni, czyli przy każdej akcji klucza na wejściu.

Byłoby na to czystszy sposób, ale prawdopodobnie najłatwiejszym sposobem byłoby przełączenie powiązania tak, aby mieć zdefiniowaną właściwość $ scope wewnątrz kontrolera, na którym działa filtr. W ten sposób możesz kontrolować, jak często ta zmienna $ scope jest aktualizowana. Coś takiego:

JS:

var App = angular.module('App', []);

App.controller('DisplayController', function($scope, $http, $timeout) {
    $http.get('data.json').then(function(result){
        $scope.entries = result.data;
    });

    // This is what you will bind the filter to
    $scope.filterText = '';

    // Instantiate these variables outside the watch
    var tempFilterText = '',
        filterTextTimeout;
    $scope.$watch('searchText', function (val) {
        if (filterTextTimeout) $timeout.cancel(filterTextTimeout);

        tempFilterText = val;
        filterTextTimeout = $timeout(function() {
            $scope.filterText = tempFilterText;
        }, 250); // delay 250 ms
    })
});

HTML:

<input id="searchText" type="search" placeholder="live search..." ng-model="searchText" />
<div class="entry" ng-repeat="entry in entries | filter:filterText">
    <span>{{entry.content}}</span>
</div>

Zwróć uwagę, że $ scope. $ Watch on an ng-modelnie będzie działał w module bootstrap angular-ui
Hendy Irawan,

1
Myślę, że będzie działać również bez zmiennej tempFilterText: $ scope. $ Watch ('searchText', function (val) {if (filterTextTimeout) $ timeout.cancel (filterTextTimeout); filterTextTimeout = $ timeout (function () {$ scope. filterText = val;}, 250); // opóźnienie 250 ms})
Jos Theeuwen

@JosTheeuwen jest to po prostu zmienna globalna, która jest uważana za złą praktykę i niedozwolona w trybie ścisłym .
mb21

301

AKTUALIZACJA

Teraz jest to łatwiejsze niż kiedykolwiek (Angular 1.3), wystarczy dodać opcję odbicia w modelu.

<input type="text" ng-model="searchStr" ng-model-options="{debounce: 1000}">

Zaktualizowany plunker:
http://plnkr.co/edit/4V13gK

Dokumentacja na temat ngModelOptions:
https://docs.angularjs.org/api/ng/directive/ngModelOptions

Stara metoda:

Oto inna metoda bez zależności poza samym angularem.

Musisz ustawić limit czasu i porównać bieżący ciąg z poprzednią wersją, jeśli obie są takie same, wykonuje wyszukiwanie.

$scope.$watch('searchStr', function (tmpStr)
{
  if (!tmpStr || tmpStr.length == 0)
    return 0;
   $timeout(function() {

    // if searchStr is still the same..
    // go ahead and retrieve the data
    if (tmpStr === $scope.searchStr)
    {
      $http.get('//echo.jsontest.com/res/'+ tmpStr).success(function(data) {
        // update the textarea
        $scope.responseData = data.res; 
      });
    }
  }, 1000);
});

i to idzie do twojego widoku:

<input type="text" data-ng-model="searchStr">

<textarea> {{responseData}} </textarea>

Obowiązkowy plunker: http://plnkr.co/dAPmwf


2
Dla mnie to dużo bardziej zrozumiała odpowiedź niż zaakceptowana :) Dzięki!
OZ_

3
Czy nie ma problemu, w którym wiele zmian modelu może się kumulować, powodując zduplikowane żądania? W odpowiedzi @ JasonAden dba o to, anulując wydarzenia z kolejki.
Blaskovicz

Teoretycznie, jeśli model ulegnie zmianie, ale dane pozostaną takie same, spowoduje to wiele żądań. W praktyce nigdy tego nie widziałem. Możesz dodać flagę, aby sprawdzić ten skrajny przypadek, jeśli się martwisz.
Josue Alexander Ibarra

To zdecydowanie lepszy wybór dla kątowej 1.3
Marcus W

Ostrzeżenie tutaj: jeśli masz zdarzenie naciśnięcia klawisza, które przesyła lub wyzwala, zrobi to bez najnowszej wartości modelu, ponieważ powiązanie wartości jest usuwane. np. wpisz „foo” i po natychmiastowym naciśnięciu klawisza zwróć wartość nadal będzie pustym łańcuchem.
jbodily

34

W Angular 1.3 zrobiłbym to:

HTML:

<input ng-model="msg" ng-model-options="{debounce: 1000}">

Kontroler:

$scope.$watch('variableName', function(nVal, oVal) {
    if (nVal !== oVal) {
        myDebouncedFunction();
    }
});

Zasadniczo mówisz angularowi, aby działał myDebouncedFunction(), gdy msgzmienna zasięgu się zmienia. Atrybut ng-model-options="{debounce: 1000}"zapewnia, że msgaktualizacja może odbywać się tylko raz na sekundę.


10
 <input type="text"
    ng-model ="criteria.searchtext""  
    ng-model-options="{debounce: {'default': 1000, 'blur': 0}}"
    class="form-control" 
    placeholder="Search" >

Teraz możemy ustawić odbijanie opcji ng-model-options w czasie, a gdy rozmycie, model musi zostać natychmiast zmieniony, w przeciwnym razie po zapisaniu będzie miał starszą wartość, jeśli opóźnienie nie zostanie zakończone.


9

Dla tych, którzy używają keyup / keydown w znacznikach HTML. To nie używa zegarka.

JS

app.controller('SearchCtrl', function ($scope, $http, $timeout) {
  var promise = '';
  $scope.search = function() {
    if(promise){
      $timeout.cancel(promise);
    }
    promise = $timeout(function() {
    //ajax call goes here..
    },2000);
  };
});

HTML

<input type="search" autocomplete="off" ng-model="keywords" ng-keyup="search()" placeholder="Search...">

6

Odrzucone / ograniczone aktualizacje modelu dla angularjs: http://jsfiddle.net/lgersman/vPsGb/3/

W twoim przypadku nie pozostaje nic więcej do zrobienia niż użycie dyrektywy w kodzie jsfiddle w następujący sposób:

<input 
    id="searchText" 
    type="search" 
    placeholder="live search..." 
    ng-model="searchText" 
    ng-ampere-debounce
/>

Zasadniczo jest to mały fragment kodu składający się z pojedynczej dyrektywy kątowej o nazwie „ng-ampere-debounce” wykorzystującej http://benalman.com/projects/jquery-throttle-debounce-plugin/, który można dołączyć do dowolnego elementu dom. Dyrektywa zmienia kolejność dołączonych programów obsługi zdarzeń, dzięki czemu może kontrolować, kiedy ograniczać zdarzenia.

Możesz go użyć do dławienia / usuwania * aktualizacji kątowych modelu * obsługi zdarzeń kątowych ng- [zdarzenie] * obsługi zdarzeń jquery

Spójrz: http://jsfiddle.net/lgersman/vPsGb/3/

Dyrektywa będzie częścią struktury Orangevolt Ampere ( https://github.com/lgersman/jquery.orangevolt-ampere ).


6

Tylko dla użytkowników przekierowanych tutaj:

Jak przedstawiono w Angular 1.3, możesz użyć atrybutu ng-model-options :

<input 
       id="searchText" 
       type="search" 
       placeholder="live search..." 
       ng-model="searchText"
       ng-model-options="{ debounce: 250 }"
/>

5

Uważam, że najlepszym sposobem rozwiązania tego problemu jest użycie wtyczki jQuery throttle / debounce Bena Almana . Moim zdaniem nie ma potrzeby opóźniania wydarzeń z każdego pola w Twoim formularzu.

Po prostu opakuj funkcję obsługi $ scope. $ Watch w $ .debounce w następujący sposób:

$scope.$watch("searchText", $.debounce(1000, function() {
    console.log($scope.searchText);
}), true);

Będziesz musiał zawrzeć to w zakresie $. $
Aplikuj

3

Innym rozwiązaniem jest dodanie funkcji opóźnienia do aktualizacji modelu. Wydaje się, że prosta dyrektywa działa:

app.directive('delayedModel', function() {
    return {
        scope: {
            model: '=delayedModel'
        },
        link: function(scope, element, attrs) {

            element.val(scope.model);

            scope.$watch('model', function(newVal, oldVal) {
                if (newVal !== oldVal) {
                    element.val(scope.model);        
                }
            });

            var timeout;
            element.on('keyup paste search', function() {
                clearTimeout(timeout);
                timeout = setTimeout(function() {
                    scope.model = element[0].value;
                    element.val(scope.model);
                    scope.$apply();
                }, attrs.delay || 500);
            });
        }
    };
});

Stosowanie:

<input delayed-model="searchText" data-delay="500" id="searchText" type="search" placeholder="live search..." />

Więc po prostu użyj delayed-modelzamiast ng-modeli zdefiniuj pożądane data-delay.

Demo: http://plnkr.co/edit/OmB4C3jtUD2Wjq5kzTSU?p=preview


Hej! czy możesz wyjaśnić, jak model: '=delayedModel'to działa? Czy możesz wskazać mi łącze, w którym mogę je znaleźć?
Akash Agrawal

@AkashAgrawal To dwukierunkowe wiązanie danych. Przeczytaj o tym tutaj docs.angularjs.org/api/ng.$compile
dfsq

1
@dfsq Używałem ng-change i uruchamiał się za każdym razem, gdy nastąpiła zmiana w tekście. Ale nie mogę go użyć, gdy zdefiniowana jest dyrektywa. element.on('change')wyzwala się tylko po rozmyciu. (1) Czy jest jakieś obejście? (2) jak wywołać funkcję kontrolera przy zmianie tekstu?
Vyas Rao,

0

Rozwiązałem ten problem za pomocą dyrektywy, która zasadniczo polega na wiązaniu rzeczywistego modelu ng ze specjalnym atrybutem, który obserwuję w dyrektywie, a następnie za pomocą usługi debounce aktualizuję atrybut dyrektywy, więc użytkownik obserwuje zmienną, która on wiąże się z modelem odbicia zamiast modelu ng.

.directive('debounceDelay', function ($compile, $debounce) {
return {
  replace: false,
  scope: {
    debounceModel: '='
  },
  link: function (scope, element, attr) {
    var delay= attr.debounceDelay;
    var applyFunc = function () {
      scope.debounceModel = scope.model;
    }
    scope.model = scope.debounceModel;
    scope.$watch('model', function(){
      $debounce(applyFunc, delay);
    });
    attr.$set('ngModel', 'model');
    element.removeAttr('debounce-delay'); // so the next $compile won't run it again!

   $compile(element)(scope);
  }
};
});

Stosowanie:

<input type="text" debounce-delay="1000" debounce-model="search"></input>

A w kontrolerze:

    $scope.search = "";
    $scope.$watch('search', function (newVal, oldVal) {
      if(newVal === oldVal){
        return;
      }else{ //do something meaningful }

Demo w jsfiddle: http://jsfiddle.net/6K7Kd/37/

usługę $ debounce można znaleźć tutaj: http://jsfiddle.net/Warspawn/6K7Kd/

Zainspirowany dyrektywą finalBind http://jsfiddle.net/fctZH/12/


0

Angular 1.3 będzie miał odbicie opcji ng-model-options, ale do tego czasu musisz używać timera, jak powiedział Josue Ibarra. Jednak w swoim kodzie uruchamia licznik czasu przy każdym naciśnięciu klawisza. Używa również setTimeout, kiedy w Angular trzeba użyć $ timeout lub użyć $ apply na końcu setTimeout.


0

Dlaczego wszyscy chcą używać zegarka? Możesz także użyć funkcji:

var tempArticleSearchTerm;
$scope.lookupArticle = function (val) {
    tempArticleSearchTerm = val;

    $timeout(function () {
        if (val == tempArticleSearchTerm) {
            //function you want to execute after 250ms, if the value as changed

        }
    }, 250);
}; 

0

Myślę, że najłatwiejszym sposobem jest wstępne załadowanie json lub załadowanie go raz, $dirtya wyszukiwanie filtru zajmie się resztą. Pozwoli to zaoszczędzić dodatkowe wywołania http i jest znacznie szybsze dzięki wstępnie załadowanym danym. Pamięć będzie bolała, ale warto.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.