Как можно использовать 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источник для примеров - это мошенничество.

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

Raynos
источник
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

});
ehynds
источник
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, того стоят, и при фактическом использовании (на основе событий пользователя) влияние на производительность не должно быть заметным.

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

Это саморекламный ответ, но я потратил несколько месяцев на его изучение и представил результаты на jQuery Conference San Francisco 2012.

Вот бесплатное видео разговора:

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

Алекс Макп
источник
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();
        }
    }
}
jcubic
источник
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.
});

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

Джон Берг
источник