jQuery deferred如何使用?

284

jQuery 1.5引入了新的Deferred对象以及附带的方法 .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
常见问题解答中提到:避免询问主观性强、每个回答都同样合理的问题,例如“你最喜欢的 ______ 是什么?”(原文有强调) - T.J. Crowder
2
@T.J.Crowser 我会考虑重新措辞。 - Raynos
5
这是一个很好的问题,但可能没有那么多人能够回答 :-) - Pointy
2
@Pointy 我主要关注那些在它还是第三方插件时使用过它的人。并鼓励大家坐下来使用它! - Raynos
1
._Deferred只是.Deferred使用的真正的“延迟对象”。它是一个内部对象,你很可能永远不会需要它。 - David Tang
显示剩余4条评论
11个回答

216

我能想到的最好用例是在缓存 AJAX 响应中。以下是来自Rebecca Murphey对该主题的介绍文章中的修改示例:

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()将非 Promise/Deferred 处理为已完成的 Promise,并立即执行链上任何.done().then()方法。

当任务可能是同步或异步操作时,Deferreds 是完美的选择,并且您希望将此条件抽象出来以便更好地管理代码。

另一个使用$.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
这是一个有用的视频,关于这个主题的内容,请访问http://www.bigbinary.com/videos/3-using-deferred-in-jquery。 - Nick Vanderbilt
5
如果结果为假值,缓存将无法起作用。此外,我不喜欢getData返回根据所选分支而有两种不同类型的事实。 - Marko Dumic
3
请参考下面Julian D.的回答,以获得更好的ajax缓存实现。 - event_jr
1
我不理解第一个代码示例是如何工作的:我理解对象未被缓存的情况,但如果它已经被缓存了,那么 cache[val] 不会返回一个 promise(jQuery 文档说参数是发送方返回的数据),这意味着 .then 的成员访问将会出错...对吗?我错过了什么? - chacham15
显示剩余7条评论

79
这是一个稍微不同于ehynd答案中的AJAX缓存实现。ehynd的答案中提到,如果在其中一个请求返回之前执行了多个重复的请求,则该实现实际上并不能防止这种情况。就是说,fortuneRice的后续问题所指出的问题仍然存在。
for (var i=0; i<3; i++) {
    getData("xxx");
}

如果之前没有缓存过"xxx"的结果,那么这很可能会导致3个AJAX请求。

可以通过缓存请求的延迟对象而不是结果来解决这个问题:

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

46
一个deferred可以代替互斥锁。这本质上与多个ajax使用场景相同。 互斥锁
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
});

当将Deferred作为互斥锁使用时,请注意其对性能的影响(http://jsperf.com/deferred-vs-mutex/2)。虽然Deferred提供的便利性和额外的好处是值得的,而且在实际(基于用户驱动事件的)使用中,性能影响不应该很明显。

对我来说,找到这个东西出奇的困难。我在一个包含setInterval的函数中使用它,该函数将返回已解决的promise并在div的宽度超过某个数字后自毁。这是为了故障排除和解决问题的解决方案,但我对此感到非常兴奋。 - JSG

29

这是一篇自我推广的回答,但我花费了几个月的时间研究, 并在2012年jQuery Conference San Francisco 上展示了结果。

以下是演讲的免费视频:

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

另一个使用 Deferred 实现任何类型计算缓存的示例(通常是一些性能密集型或长时间运行的任务):

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浏览器将为调整窗口大小的每个像素触发resize事件,而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);
});

这将序列化您的代码执行,以便按照您预期的方式执行。当将对象方法作为回调传递给延迟对象时,请注意陷阱。一旦此类方法作为延迟回调执行,'this'引用将被覆盖为延迟对象的引用,并且不再引用该方法所属的对象。


这个怎么进行序列化?你已经解决了队列,所以resizeQueue.done(resizeAlgorithm)resizeAlgorithm完全一样。这是一个彻头彻尾的骗局! - Raynos
当你的resizeAlgorithm代码很复杂时,每次调整窗口大小时JavaScript在webkit中的实现会失去同步。延迟将您的回调保留在队列中,并按先进先出的顺序执行它们。因此,如果您添加了一个“完成”回调并且它立即执行,因为延迟已经解决,而在第一个回调仍在执行时添加到延迟中的另一个“完成”回调将被添加到队列中,并且必须等待第一个回调返回。希望这能回答你的问题。 - Miloš Rašić
浏览器中的JS解释器是单线程的。除非你的resizeAlgorithm函数内部有一些异步代码,否则在下一次调用.done之前整个函数应该已经完成操作。 - Raynos
@Raynos: 我知道那个,但是我尝试在调整大小时简单地调用resizeAlgorithm,在WebKit浏览器中它会给出一个空白白页,而在其他浏览器中则完美工作。使用deferred可以解决这个问题。我还没有足够的时间进行深入研究。可能是WebKit的一个bug。如果resizeAlgorithm有一些异步代码,我不认为我的示例中使用的deferred会有所帮助。 - Miloš Rašić
2
你不应该使用像节流/防抖插件 http://benalman.com/projects/jquery-throttle-debounce-plugin/ 这样的东西来防止函数在调整大小时触发多次。 - wheresrhys

2

您还可以将其与任何使用JQuery的第三方库集成。

其中一个这样的库是Backbone,实际上在他们的下一个版本中将支持Deferred。


2
使用“阅读更多”代替“在我的博客上”更好,这是一种更好的做法,可以避免您的答案被(意外地)标记为垃圾邮件。 :) - Lokesh Mehra

1
ehynds的答案不可行,因为它缓存了响应数据。应该缓存jqXHR,它也是一个Promise。 以下是正确的代码:
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.
});

朱利安·D的答案是正确的,并且是更好的解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接