Як можна використовувати відкладені jQuery?


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 Я в основному дивлюся на тих, хто ним користувався, коли це був плагін сторонніх розробників. І заохочуючи людей сідати і користуватися ним!
Райнос

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
Два приклади блискучих. Я реалізував щось подібне до другого, але з 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помилка доступу учасника буде ... правда? Що я пропускаю?
чача15

79

Ось дещо інша реалізація кеша AJAX, як у відповіді ехінда .

Як зазначається у подальшому питанні fortuneRice, реалізація ehynd насправді не завадила декільком однаковим запитам, якщо запити виконувались до того, як один із них повернувся. Це є,

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

швидше за все, це призведе до 3 запитів AJAX, якщо результат для "xxx" раніше не кешований.

Це можна вирішити кешуванням відкладених запитів замість результату:

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

ВИЗНАЧЕНО

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

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


Мені це було дивно важко знайти. Я використав його для функції, що містить setInterval, який би повернув розв’язану обіцянку і самознищився, як тільки ширина діва зробила це протягом певного числа. Це було для вирішення неполадок і рішення, якщо я не зміг вирішити свою проблему, але я в цьому захоплююсь.
JSG


20

Ще одне використання, яке я ставив перед собою, - це отримання даних з різних джерел. У наведеному нижче прикладі я отримую кілька незалежних об'єктів схеми JSON, які використовуються в існуючій програмі для перевірки між клієнтом та сервером REST. У цьому випадку я не хочу, щоб програма на стороні браузера починала завантажувати дані, перш ніж у неї будуть завантажені всі схеми. $ .when.apply (). then () ідеально підходить для цього. Дякуємо Raynos за покажчики на використання тоді (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 запускають подію розміру для кожного пікселя, вікно змінюється на відміну, на відміну від 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);
});

Це дозволить серіалізувати виконання вашого коду так, щоб він виконувався так, як ви його задумали. Остерігайтеся підводних каменів при передачі об'єктних методів як зворотних дзвінків до відкладених. Після того, як такий метод буде виконаний як зворотний виклик до відкладеного, посилання "це" буде перезаписано з посиланням на відкладений об'єкт і більше не буде посилатися на об'єкт, якому належить метод.


Як це робить будь-яка серіалізація? Ви вже вирішили чергу, так resizeQueue.done(resizeAlgorithm)само, як і resizeAlgorithm. Це повне шахрайство!
Райнос

Коли код вашого resizeAlgorithm є складним, реалізація JavaScript у веб-програмі втратить синхронізацію, коли функція викликається для кожного пікселя, який ви зміните розмір вікна. Відкладений зберігає ваші зворотні дзвінки в черзі та виконує їх у порядку FIFO. Отже, якщо ви додали зворотний виклик "зроблено", і він виконується негайно, оскільки відкладений вже вирішено, до черги буде доданий ще один "виконаний" зворотний виклик, який додається до відкладеного, поки перший зворотний виклик все ще виконується, і доведеться чекати щоб повернутися до першого зворотного дзвінка. Я сподіваюся, що це відповість на ваше запитання.
Мілош Рашич

Інтерпретатор JS у браузері є однопоточним. Якщо ваш resizeAlgorithm не має в собі деякого асинхронного коду, вся функція повинна була закінчити роботу до наступного виклику .done.
Райнос

@Raynos: Я знаю про це, але я спробував просто зателефонувати на resizeAlgorithm на розмір, і це дає порожню білу сторінку в веб-браузерах, працюючи ідеально в інших. Відкладене вирішує цю проблему. У мене не було достатньо часу для глибшого дослідження цього. Може бути помилкою у веб-коді. Я не думаю, що відкладене, як використано в моєму прикладі, не допомогло б, якщо в resizeAlgorithm був якийсь асинхронний код.
Мілош Рашич

2
Якщо ви не використовуєте щось на кшталт плагіна дроселя / дебютування benalman.com/projects/jquery-throttle-debounce-plugin, щоб запобігти запуску функцій більше тана один раз за розмір.
wheresrhys

2

Ви також можете інтегрувати його з будь-якими сторонніми бібліотеками, які використовують JQuery.

Однією з таких бібліотек є Backbone, яка насправді буде підтримувати відкладені в їх наступній версії.


2
Використання read more hereзамість on my blog. Це краща практика і може врятувати вас від випадкового спаму. :)
Локеш Мехра

1

Я щойно використовував Відкладений у реальному коді. У проекті jQuery Terminal у мене є функція exec, яка викликає команди, визначені користувачем (наприклад, він вводив його та натискав клавішу Enter), я додав відкладені в 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.