Как можно использовать jQuery deferred?


279

jQuery 1.5 приносит новый отложенный объект и присоединенные методы .when, .Deferredи ._Deferred.

Для тех, кто не использовал .Deferredраньше, я аннотировал источник для этого .

Каковы возможные применения этих новых методов, как мы можем приспособить их к шаблонам?

Я уже прочитал API и исходный код , поэтому я знаю, что он делает. Мой вопрос: как мы можем использовать эти новые функции в повседневном коде?

У меня есть простой пример буферного класса, который вызывает AJAX-запрос по порядку. (Следующий начинается после окончания предыдущего).

/* 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();
    };
};

Я ищу демонстрации и возможное использование .Deferredи .when.

Также было бы здорово увидеть примеры ._Deferred.

Ссылка на новый jQuery.ajaxисточник для примеров - это мошенничество.

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


19
Из FAQ: не задавайте субъективных вопросов, где ... каждый ответ одинаково действителен: «Какой твой любимый ______?» (их акцент)
— TJ Crowder

2
@TJCrowser Я посмотрю на переписывание.
— Рэйнос

5
Это хороший вопрос , но не может быть , что многие люди , которые могут ответить :-)
— Понтий

2
@Pointy Я в основном смотрю на тех, кто использовал его, когда это был сторонний плагин. И поощряя людей сесть и использовать это!
— Raynos

1
._Deferredэто просто истинный «отложенный объект», который .Deferredиспользует. Это внутренний объект, который вам, скорее всего, никогда не понадобится.
— Дэвид Тан

Ответы:


212

Лучший вариант использования, который я могу придумать, это кэширование ответов AJAX. Вот модифицированный пример из вступительного поста Ребекки Мерфи на эту тему :

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.
});

В основном, если значение уже было запрошено один раз, прежде чем оно немедленно возвращается из кэша. В противном случае запрос AJAX извлекает данные и добавляет их в кэш. $.when/ .thenНе заботится обо всем этом; все, что вам нужно, это использовать ответ, который передается .then()обработчику в обоих случаях. jQuery.when()обрабатывает не-Обещание / Отложено как Завершенное, немедленно выполняя любое .done()или .then()в цепочке.

Отложенные идеально подходят для случаев, когда задача может работать или не работать асинхронно, и вы хотите абстрагировать это условие от кода.

Еще один пример из реальной жизни с использованием $.whenпомощника:

$.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

});

4
Два примера бриллиантов. Я реализовал что-то похожее на 2-й, но с 4-мя ajax-запросами, и он хорошо работает, в дополнение к тому, что он гораздо более разборчивый, компактный, логичный, обслуживаемый и т. Д. JQuery.Deferred - действительно хорошая вещь.
— PJP

5
Вот полезное видео на эту тему. Bigbinary.com/videos/3-using-deferred-in-jquery
— Ник Вандербильт,

5
Кэширование не будет работать, если результатом будет ложное значение. Также мне не нравится тот факт, что getData возвращает 2 разных типа в зависимости от выбранной ветви.
— Марко Думик

3
См. Ответ Джулиана Д. ниже для лучшей реализации Ajax-кэширования.
— event_jr

1
Я не понимаю, как работает первый пример кода: я понимаю случай, когда объект не кэшируется, но если он не будет cache[ val ]возвращать обещание (документация jquery говорит, что параметром являются данные, возвращаемые отправителем), это означает, что член доступа .thenбудет ошибка ... правильно? Чего мне не хватает?
— chacham15

79

Вот немного отличная реализация AJAX-кэша, как в ответе Ehynd .

Как отмечено в последующем вопросе к fortuneRice, реализация ehynd на самом деле не предотвращала множественные идентичные запросы, если запросы были выполнены до того, как один из них вернулся. То есть,

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

скорее всего, приведет к 3 AJAX-запросам, если результат для «xxx» еще не был кэширован ранее.

Это может быть решено путем кэширования Deferreds запроса вместо результата:

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.
});

1
Я думаю, что это все еще не идеально, так как вы никогда не очищаете / не обновляете кэш после первого извлечения. Это сделает вызов AJAX не работающим для любого обновления.
— zyzyis

45

Отложенный может быть использован вместо мьютекса. По сути, это то же самое, что и сценарии множественного использования ajax.

MUTEX

var mutex = 2;

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

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

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

DEFERRED

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
});

При использовании Deferred в качестве мьютекса, следите за влиянием производительности (http://jsperf.com/deferred-vs-mutex/2). Хотя удобство, а также дополнительные преимущества, предоставляемые Deferred, того стоят, и при фактическом использовании (на основе событий пользователя) влияние на производительность не должно быть заметным.


Мне было удивительно трудно найти это. Я использовал его для функции, содержащей setInterval, которая будет возвращать разрешенное обещание и самоуничтожается, как только ширина div превысит определенное число. Это было для устранения неполадок и решения, если я не мог решить свою проблему, но я в восторге от этого.
— JSG


20

Еще одно использование, которое я использовал для достижения хороших целей, - это получение данных из нескольких источников. В приведенном ниже примере я извлекаю несколько независимых объектов схемы JSON, используемых в существующем приложении для проверки между клиентом и сервером REST. В этом случае я не хочу, чтобы приложение на стороне браузера начинало загружать данные до того, как загрузятся все схемы. $ .when.apply (). then () идеально подходит для этого. Спасибо Raynos за указатели на использование then (fn1, fn2) для отслеживания ошибок.

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);
});     

10

Другой пример использования Deferreds для реализации кэша для любого вида вычислений (обычно это некоторые задачи с высокой производительностью или длительные задачи):

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("|");
};

Вот пример использования этого класса для выполнения некоторых (смоделированных тяжелых) вычислений:

// 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);
});

Тот же базовый кеш можно использовать для кеширования 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);
});

Вы можете играть с приведенным выше кодом в этом jsFiddle .


9

1) Используйте его для обеспечения упорядоченного выполнения обратных вызовов:

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) Используйте его, чтобы проверить статус приложения:

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

jQuery.when(loggedIn, databaseReady).then(function() {
  //do something
});

2

Вы можете использовать отложенный объект для создания плавного дизайна, который хорошо работает в браузерах webkit. Браузеры Webkit будут запускать событие изменения размера для каждого пикселя, в котором изменяется размер окна, в отличие от FF и IE, которые запускают событие только один раз для каждого изменения размера. В результате вы не можете контролировать порядок выполнения функций, связанных с вашим событием изменения размера окна. Нечто подобное решает проблему:

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);
});

Это выполнит сериализацию выполнения вашего кода так, чтобы он выполнялся так, как вы намеревались. Остерегайтесь ловушек при передаче методов объекта как обратных вызовов к отложенному. Как только такой метод будет выполнен как обратный вызов для deferred, ссылка 'this' будет перезаписана со ссылкой на объект deferred и больше не будет ссылаться на объект, которому принадлежит метод.


Как это делает сериализацию? Вы уже решили очередь, так resizeQueue.done(resizeAlgorithm)же, как и resizeAlgorithm. Это полный обман!
— Raynos

Когда код вашего resizeAlgorithm сложный, реализация JavaScript в webkit потеряет синхронизацию при вызове функции для каждого пикселя, который вы изменяете в размере окна. Отложенный сохраняет ваши обратные вызовы в очереди и выполняет их в порядке FIFO. Таким образом, если вы добавляете обратный вызов «done» и он выполняется немедленно, потому что отложенный вызов уже разрешен, в очередь будет добавлен еще один обратный вызов «done», который добавляется к отложенному, пока еще выполняется первый обратный вызов, и ему придется ждать для первого обратного вызова, чтобы вернуться. Надеюсь, это ответит на ваш вопрос.
— Милош Рашич

интерпретатор JS в браузере однопоточный. Если ваш resizeAlgorithm не имеет некоторого асинхронного кода внутри, вся функция должна была закончить работу до того, как .doneбудет сделан следующий вызов .
— Raynos

@Raynos: Я знаю об этом, но я попытался просто вызвать resizeAlgorithm при изменении размера, и он дает пустую белую страницу в браузерах webkit, в то же время прекрасно работая в других. Отсроченный решает эту проблему. У меня не было достаточно времени, чтобы провести более глубокое исследование этого. Может быть, ошибка веб-набора. Я не думаю, что deferred, как используется в моем примере, помог бы, если бы resizeAlgorithm имел некоторый асинхронный код.
— Милош Рашич

2
Разве вы не должны использовать что-то вроде плагина throttle / debounce benalman.com/projects/jquery-throttle-debounce-plugin, чтобы ваши функции не запускались больше одного раза за изменение размера.
— Wheresrhys

2

Вы также можете интегрировать его с любыми сторонними библиотеками, использующими JQuery.

Одна из таких библиотек - Backbone, которая на самом деле собирается поддерживать Deferred в следующей версии.


2
Используйте read more hereвместо on my blog. Это лучшая практика и может спасти вас от (случайно) спам. :)
— Локеш Мехра

1

Я только что использовал Deferred в реальном коде. В проекте jQuery Terminal у меня есть функция exec, которая вызывает команды, определенные пользователем (например, он вводил его и нажимал ввод), я добавил Deferreds в API и вызывал exec с массивами. как это:

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

или

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

Команды могут запускать асинхронный код, и exec должен вызывать код пользователя по порядку. Мой первый API использует пару вызовов паузы / возобновления, и в новом API я вызываю их автоматически, когда пользователь возвращает обещание. Так что пользовательский код можно просто использовать

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

или

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

Я использую такой код:

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 используются в функции возобновления, которая снова вызывает exec со всеми командами dalyed_commands.

и часть функции команд (я удалил не связанные части)

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();
        }
    }
}

1

Ответ по ehynds не будет работать, потому что он кэширует данные ответов. Он должен кэшировать jqXHR, который также является Обещанием. Вот правильный код:

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.
});

Ответ Джулиана Д. будет работать правильно и является лучшим решением.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.