Jak można odłożyć jQuery?

279

jQuery 1,5 przynosi nowe odroczony przedmiot i dołączone metody .when, .Deferredi ._Deferred.

Dla tych, którzy nie używali .Deferredwcześniej, mam uwagami na źródło dla niego .

Jakie są możliwe zastosowania tych nowych metod, w jaki sposób zajmiemy się dopasowaniem ich do wzorców?

Przeczytałem już interfejs API i źródło , więc wiem, co robi. Moje pytanie brzmi: w jaki sposób możemy korzystać z tych nowych funkcji w codziennym kodzie?

Mam prosty przykład klasy bufora, która po kolei wywołuje żądanie AJAX. (Następny zaczyna się po zakończeniu poprzedniego).

/* Class: Buffer
 *  methods: append
 *
 *  Constructor: takes a function which will be the task handler to be called
 *
 *  .append appends a task to the buffer. Buffer will only call a task when the 
 *  previous task has finished
 */
var Buffer = function(handler) {
    var tasks = [];
    // empty resolved deferred object
    var deferred = $.when();

    // handle the next object
    function handleNextTask() {
        // if the current deferred task has resolved and there are more tasks
        if (deferred.isResolved() && tasks.length > 0) {
            // grab a task
            var task = tasks.shift();
            // set the deferred to be deferred returned from the handler
            deferred = handler(task);
            // if its not a deferred object then set it to be an empty deferred object
            if (!(deferred && deferred.promise)) {
                deferred = $.when();
            }
            // if we have tasks left then handle the next one when the current one 
            // is done.
            if (tasks.length > 0) {
                deferred.done(handleNextTask);
            }
        }
    }

    // appends a task.
    this.append = function(task) {
        // add to the array
        tasks.push(task);
        // handle the next task
        handleNextTask();
    };
};

Szukam demonstracji i możliwych zastosowań .Deferredi .when.

Byłoby również miło zobaczyć przykłady ._Deferred.

Powiązanie z nowym jQuery.ajaxźródłem przykładów to oszustwo.

Szczególnie interesuje mnie, jakie techniki są dostępne, gdy wyodrębniamy, czy operacja jest wykonywana synchronicznie, czy asynchronicznie.

Raynos
źródło
19
Z FAQ: unikaj zadawania subiektywnych pytań, gdzie ... każda odpowiedź jest równie ważna: „Jaki jest twój ulubiony ______?” (ich podkreślenie)
TJ Crowder
2
@TJCrowser Popatrzę na przeredagowanie go.
Raynos
5
To dobre pytanie, ale nie może być tak wielu ludzi, którzy mogą odpowiedzieć :-)
Pointy
2
@Pointy Patrzę głównie na tych, którzy korzystali z niej, gdy była to wtyczka innej firmy. I zachęcanie ludzi do siedzenia i korzystania z niego!
Raynos
1
._Deferredjest po prostu prawdziwym „Odroczonym obiektem”, który .Deferredwykorzystuje. To wewnętrzny obiekt, którego najprawdopodobniej nigdy nie będziesz potrzebować.
David Tang

Odpowiedzi:

212

Najlepszy przypadek użycia, jaki mogę wymyślić, to buforowanie odpowiedzi AJAX. Oto zmodyfikowany przykład z postu wprowadzającego Rebecca Murphey na ten temat :

var cache = {};

function getData( val ){

    // return either the cached value or jqXHR object wrapped Promise
    return $.when(
        cache[ val ] || 
        $.ajax('/foo/', {
            data: { value: val },
            dataType: 'json',
            success: function( resp ){
                cache[ val ] = resp;
            }
        })
    );
}

getData('foo').then(function(resp){
    // do something with the response, which may
    // or may not have been retrieved using an
    // XHR request.
});

Zasadniczo, jeśli wartość została już raz zażądana, zanim zostanie natychmiast zwrócona z pamięci podręcznej. W przeciwnym razie żądanie AJAX pobiera dane i dodaje je do pamięci podręcznej. $.when/ .thenNie dba o nic z tego; wszystko, o co musisz się martwić, to skorzystanie z odpowiedzi, która jest przekazywana do programu .then()obsługi w obu przypadkach. jQuery.when()obsługuje niezrealizowaną / odroczoną jako zakończoną, natychmiast wykonując dowolną .done()lub .then()w łańcuchu.

Odroczenia są idealne na wypadek, gdy zadanie może działać asynchronicznie lub nie, a chcesz wyodrębnić ten warunek z kodu.

Kolejny przykład z użyciem $.whenpomocnika:

$.when($.getJSON('/some/data/'), $.get('template.tpl')).then(function (data, tmpl) {

    $(tmpl) // create a jQuery object out of the template
    .tmpl(data) // compile it
    .appendTo("#target"); // insert it into the DOM

});
ehynds
źródło
4
Dwa przykłady brylantów. Zaimplementowałem coś podobnego do drugiego, ale z 4 żądaniami ajax i działa dobrze, oprócz tego, że jest o wiele bardziej czytelny, zwarty, logiczny, łatwy w utrzymaniu itp. JQuery.Deferred to naprawdę dobra rzecz.
PJP
5
Oto przydatny film na ten temat bigbinary.com/videos/3-using-deferred-in-jquery
Nick Vanderbilt
5
Buforowanie nie będzie działać, jeśli wynikiem będzie wartość fałsz. Nie podoba mi się również fakt, że getData zwraca 2 różne typy w zależności od branej gałęzi.
Marko Dumic
3
Zobacz odpowiedź Juliana D. poniżej, aby uzyskać lepszą implementację buforowania ajax.
event_jr
1
Nie rozumiem nawet, jak działa pierwszy przykład kodu: rozumiem przypadek, w którym obiekt nie jest buforowany, ale jeśli tak, to cache[ val ]NIE zwróci obietnicy (dokumentacja jquery mówi, że parametr to dane zwrócone przez nadawcę), co oznacza, że dostęp członka .thenbędzie błąd ... prawda? czego mi brakuje?
chacham15
79

Oto nieco inna implementacja pamięci podręcznej AJAX niż w odpowiedzi ehynd .

Jak zauważono w pytaniu uzupełniającym fortuneRice, implementacja ehynd nie zapobiegła wielu identycznym żądaniom, jeśli żądania zostały wykonane przed zwrotem jednego z nich. To jest,

for (var i=0; i<3; i++) {
    getData("xxx");
}

najprawdopodobniej spowoduje 3 żądania AJAX, jeśli wynik dla „xxx” nie był wcześniej buforowany.

Można to rozwiązać, buforując odroczone żądanie zamiast wyniku:

var cache = {};

function getData( val ){

    // Return a promise from the cache (if available)
    // or create a new one (a jqXHR object) and store it in the cache.
    var promise = cache[val];
    if (!promise) {
        promise = $.ajax('/foo/', {
            data: { value: val },
            dataType: 'json'
        });
        cache[val] = promise;
    }
    return promise;
}

$.when(getData('foo')).then(function(resp){
    // do something with the response, which may
    // or may not have been retreived using an
    // XHR request.
});
Julian D.
źródło
1
Myślę, że nadal nie jest to idealne, ponieważ nigdy nie wyczyścisz / nie zaktualizujesz pamięci podręcznej po pierwszym pobraniu. Spowoduje to, że wywołanie AJAX nie będzie działać dla żadnej aktualizacji.
zyzyis
45

Odroczenie może być użyte zamiast muteksu. Jest to w zasadzie to samo co scenariusze użycia wielu aukcji.

MUTEX

var mutex = 2;

setTimeout(function() {
 callback();
}, 800);

setTimeout(function() {
 callback();
}, 500);

function callback() {
 if (--mutex === 0) {
  //run code
 }
}

ODROCZONY

function timeout(x) {
 var dfd = jQuery.Deferred();
 setTimeout(function() {
  dfd.resolve();
 }, x);
 return dfd.promise();
}

jQuery.when(
timeout(800), timeout(500)).done(function() {
 // run code
});

Używając opcji Odroczony jako muteksu, uważaj na wpływ na wydajność (http://jsperf.com/deferred-vs-mutex/2). Chociaż wygoda, a także dodatkowe korzyści zapewniane przez Odroczone są tego warte, a faktyczne (oparte na zdarzeniach użytkownika) wykorzystanie wydajności nie powinno być zauważalne.

użytkownik406905
źródło
Zaskakująco trudno było mi to znaleźć. Użyłem go w funkcji zawierającej setInterval, która zwróciłaby rozwiązaną obietnicę i uległa samozniszczeniu, gdy szerokość div przekroczyła określoną liczbę. To było na rozwiązywanie problemów i rozwiązanie, jeśli nie mogłem rozwiązać problemu, ale jestem zachwycony.
JSG
28

To jest autopromocyjna odpowiedź, ale spędziłem kilka miesięcy badając ją i prezentując wyniki na konferencji jQuery w San Francisco 2012.

Oto bezpłatny film z rozmowy:

https://www.youtube.com/watch?v=juRtEEsHI9E

Alex Mcp
źródło
20

Kolejnym zastosowaniem, który wykorzystałem w dobrym celu, jest pobieranie danych z wielu źródeł. W poniższym przykładzie pobieram wiele niezależnych obiektów schematu JSON używanych w istniejącej aplikacji do sprawdzania poprawności między klientem a serwerem REST. W tym przypadku nie chcę, aby aplikacja po stronie przeglądarki rozpoczynała ładowanie danych, zanim załadowane zostaną wszystkie schematy. $ .when.apply (). then () jest do tego idealny. Dziękujemy Raynosowi za wskazówki dotyczące używania wtedy (fn1, fn2) do monitorowania warunków błędów.

fetch_sources = function (schema_urls) {
    var fetch_one = function (url) {
            return $.ajax({
                url: url,
                data: {},
                contentType: "application/json; charset=utf-8",
                dataType: "json",
            });
        }
    return $.map(schema_urls, fetch_one);
}

var promises = fetch_sources(data['schemas']);
$.when.apply(null, promises).then(

function () {
    var schemas = $.map(arguments, function (a) {
        return a[0]
    });
    start_application(schemas);
}, function () {
    console.log("FAIL", this, arguments);
});     
Elf Sternberg
źródło
10

Kolejny przykład użycia Deferreds do zaimplementowania pamięci podręcznej dla dowolnego rodzaju obliczeń (zwykle niektóre zadania wymagające dużej wydajności lub długotrwałe):

var ResultsCache = function(computationFunction, cacheKeyGenerator) {
    this._cache = {};
    this._computationFunction = computationFunction;
    if (cacheKeyGenerator)
        this._cacheKeyGenerator = cacheKeyGenerator;
};

ResultsCache.prototype.compute = function() {
    // try to retrieve computation from cache
    var cacheKey = this._cacheKeyGenerator.apply(this, arguments);
    var promise = this._cache[cacheKey];

    // if not yet cached: start computation and store promise in cache 
    if (!promise) {
        var deferred = $.Deferred();
        promise = deferred.promise();
        this._cache[cacheKey] = promise;

        // perform the computation
        var args = Array.prototype.slice.call(arguments);
        args.push(deferred.resolve);
        this._computationFunction.apply(null, args);
    }

    return promise;
};

// Default cache key generator (works with Booleans, Strings, Numbers and Dates)
// You will need to create your own key generator if you work with Arrays etc.
ResultsCache.prototype._cacheKeyGenerator = function(args) {
    return Array.prototype.slice.call(arguments).join("|");
};

Oto przykład użycia tej klasy do wykonania niektórych (symulowanych ciężkich) obliczeń:

// The addingMachine will add two numbers
var addingMachine = new ResultsCache(function(a, b, resultHandler) {
    console.log("Performing computation: adding " + a + " and " + b);
    // simulate rather long calculation time by using a 1s timeout
    setTimeout(function() {
        var result = a + b;
        resultHandler(result);
    }, 1000);
});

addingMachine.compute(2, 4).then(function(result) {
    console.log("result: " + result);
});

addingMachine.compute(1, 1).then(function(result) {
    console.log("result: " + result);
});

// cached result will be used
addingMachine.compute(2, 4).then(function(result) {
    console.log("result: " + result);
});

Tej samej pamięci podręcznej można użyć do buforowania żądań Ajax:

var ajaxCache = new ResultsCache(function(id, resultHandler) {
    console.log("Performing Ajax request for id '" + id + "'");
    $.getJSON('http://jsfiddle.net/echo/jsonp/?callback=?', {value: id}, function(data) {
        resultHandler(data.value);
    });
});

ajaxCache.compute("anID").then(function(result) {
    console.log("result: " + result);
});

ajaxCache.compute("anotherID").then(function(result) {
    console.log("result: " + result);
});

// cached result will be used
ajaxCache.compute("anID").then(function(result) {
    console.log("result: " + result);
});

Możesz grać z powyższym kodem w tym jsFiddle .

Julian D.
źródło
9

1) Użyj go, aby zapewnić uporządkowane wykonanie oddzwaniania:

var step1 = new Deferred();
var step2 = new Deferred().done(function() { return step1 });
var step3 = new Deferred().done(function() { return step2 });

step1.done(function() { alert("Step 1") });
step2.done(function() { alert("Step 2") });
step3.done(function() { alert("All done") });
//now the 3 alerts will also be fired in order of 1,2,3
//no matter which Deferred gets resolved first.

step2.resolve();
step3.resolve();
step1.resolve();

2) Użyj go, aby zweryfikować status aplikacji:

var loggedIn = logUserInNow(); //deferred
var databaseReady = openDatabaseNow(); //deferred

jQuery.when(loggedIn, databaseReady).then(function() {
  //do something
});
Kernel James
źródło
2

Możesz użyć odroczonego obiektu, aby stworzyć płynny projekt, który działa dobrze w przeglądarkach webkit. Przeglądarki Webkit będą uruchamiać zdarzenie zmiany rozmiaru dla każdego piksela, którego rozmiar jest zmieniany, w przeciwieństwie do FF i IE, które uruchamiają zdarzenie tylko raz dla każdej zmiany rozmiaru. W rezultacie nie masz kontroli nad kolejnością wykonywania funkcji związanych ze zdarzeniem zmiany rozmiaru okna. Coś takiego rozwiązuje problem:

var resizeQueue = new $.Deferred(); //new is optional but it sure is descriptive
resizeQueue.resolve();

function resizeAlgorithm() {
//some resize code here
}

$(window).resize(function() {
    resizeQueue.done(resizeAlgorithm);
});

Spowoduje to serializację wykonania kodu, tak aby działał zgodnie z przeznaczeniem. Uważaj na pułapki przy przekazywaniu metod obiektowych jako oddzwaniania do odroczonego. Gdy taka metoda zostanie wykonana jako wywołanie zwrotne do odroczenia, odniesienie „to” zostanie zastąpione odniesieniem do odroczonego obiektu i nie będzie już odnosiło się do obiektu, do którego należy metoda.

Miloš Rašić
źródło
Jak to robi jakąkolwiek serializację? Już rozwiązałeś kolejkę, więc resizeQueue.done(resizeAlgorithm)jest dokładnie tak samo jak resizeAlgorithm. To kompletne oszustwo!
Raynos
Gdy kod Twojego algorytmu resizeAlgorytm jest złożony, implementacja JavaScript w pakiecie internetowym utraci synchronizację, gdy funkcja zostanie wywołana dla każdego piksela, którego rozmiar zmienia się w oknie. Odroczony utrzymuje oddzwanianie w kolejce i wykonuje je w kolejności FIFO. Jeśli więc dodasz wywołanie zwrotne „gotowe” i zostanie ono wykonane natychmiast, ponieważ odroczone jest już rozwiązane, kolejne wywołanie zwrotne „gotowe”, które zostanie dodane do odroczonego podczas pierwszego wykonywania, zostanie dodane do kolejki i będzie musiało poczekać do pierwszego połączenia zwrotnego. Mam nadzieję, że to odpowiada na twoje pytanie.
Miloš Rašić
interpreter JS w przeglądarce jest jednowątkowy. O ile twój resizeAlgorytm nie zawiera kodu asynchronicznego, cała funkcja powinna zakończyć działanie przed wykonaniem następnego wywołania .done.
Raynos
@Raynos: Zdaję sobie z tego sprawę, ale próbowałem po prostu wywołać algorytm resizeAlgorytm przy zmianie rozmiaru i daje pustą białą stronę w przeglądarkach webkit, podczas gdy działa doskonale w innych. Odroczony rozwiązuje ten problem. Nie miałem czasu na głębsze badania w tym zakresie. Może to być błąd webkita. Nie sądzę, aby odroczenie zastosowane w moim przykładzie pomogło, gdyby resizeAlgorithm zawierał kod asynchroniczny.
Miloš Rašić
2
Nie powinieneś używać czegoś takiego jak wtyczka przepustnicy / debounce benalman.com/projects/jquery-throttle-debounce-plugin, aby zapobiec uruchamianiu większej liczby funkcji raz na zmianę rozmiaru.
wheresrhys
2

Możesz także zintegrować go z dowolnymi bibliotekami innych firm korzystającymi z JQuery.

Jedną z takich bibliotek jest Backbone, która w rzeczywistości będzie obsługiwać Odroczoną w ich następnej wersji.

Diego
źródło
2
Użyj read more herezamiast on my blog. Jest to lepsza praktyka i może uchronić Cię przed (przypadkowym) spamowaniem. :)
Lokesh Mehra
1

Właśnie użyłem opcji Odroczony w prawdziwym kodzie. W projekcie jQuery Terminal mam funkcję exec, która wywołuje polecenia zdefiniowane przez użytkownika (tak jak on wchodził do niej i naciskał enter), dodałem Deferreds do API i wywołuje exec z tablicami. lubię to:

terminal.exec('command').then(function() {
   terminal.echo('command finished');
});

lub

terminal.exec(['command 1', 'command 2', 'command 3']).then(function() {
   terminal.echo('all commands finished');
});

polecenia mogą uruchamiać kod asynchroniczny, a exec musi wywoływać kod użytkownika w kolejności. Mój pierwszy interfejs API wykorzystuje parę połączeń pauzujących / wznawiających, aw nowym interfejsie API wywołuję te automatycznie, gdy użytkownik zwróci obietnicę. Więc kod użytkownika może po prostu użyć

return $.get('/some/url');

lub

var d = new $.Deferred();
setTimeout(function() {
    d.resolve("Hello Deferred"); // resolve value will be echoed
}, 500);
return d.promise();

Używam takiego kodu:

exec: function(command, silent, deferred) {
    var d;
    if ($.isArray(command)) {
        return $.when.apply($, $.map(command, function(command) {
            return self.exec(command, silent);
        }));
    }
    // both commands executed here (resume will call Term::exec)
    if (paused) {
        // delay command multiple time
        d = deferred || new $.Deferred();
        dalyed_commands.push([command, silent, d]);
        return d.promise();
    } else {
        // commands may return promise from user code
        // it will resolve exec promise when user promise
        // is resolved
        var ret = commands(command, silent, true, deferred);
        if (!ret) {
            if (deferred) {
                deferred.resolve(self);
                return deferred.promise();
            } else {
                d = new $.Deferred();
                ret = d.promise();
                ret.resolve();
            }
        }
        return ret;
    }
},

dalyed_commands jest używany w funkcji wznawiania, która ponownie wywołuje exec ze wszystkimi dalyed_commands.

i część funkcji poleceń (usunąłem niepowiązane części)

function commands(command, silent, exec, deferred) {

    var position = lines.length-1;
    // Call user interpreter function
    var result = interpreter.interpreter(command, self);
    // user code can return a promise
    if (result != undefined) {
        // new API - auto pause/resume when using promises
        self.pause();
        return $.when(result).then(function(result) {
            // don't echo result if user echo something
            if (result && position === lines.length-1) {
                display_object(result);
            }
            // resolve promise from exec. This will fire
            // code if used terminal::exec('command').then
            if (deferred) {
                deferred.resolve();
            }
            self.resume();
        });
    }
    // this is old API
    // if command call pause - wait until resume
    if (paused) {
        self.bind('resume.command', function() {
            // exec with resume/pause in user code
            if (deferred) {
                deferred.resolve();
            }
            self.unbind('resume.command');
        });
    } else {
        // this should not happen
        if (deferred) {
            deferred.resolve();
        }
    }
}
jcubic
źródło
1

Odpowiedź ehynds nie będzie działać, ponieważ buforuje dane odpowiedzi. Powinien buforować jqXHR, który jest również obietnicą. Oto poprawny kod:

var cache = {};

function getData( val ){

    // return either the cached value or an
    // jqXHR object (which contains a promise)
    return cache[ val ] || $.ajax('/foo/', {
        data: { value: val },
        dataType: 'json',
        success: function(data, textStatus, jqXHR){
            cache[ val ] = jqXHR;
        }
    });
}

getData('foo').then(function(resp){
    // do something with the response, which may
    // or may not have been retreived using an
    // XHR request.
});

Odpowiedź Juliana D. będzie działać poprawnie i jest lepszym rozwiązaniem.

John Berg
źródło