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
จากคำถามที่พบบ่อย: หลีกเลี่ยงการถามคำถามแบบอัตนัยที่ ... ทุกคำตอบมีความถูกต้องเท่ากัน:“ คุณชอบ ______ อะไร” (เน้นของพวกเขา)
TJ Crowder

2
@TJCrowser ฉันจะดู rewording มัน
Raynos

5
มันเป็นคำถามที่ดี แต่มีไม่สามารถเป็นที่คนจำนวนมากที่สามารถตอบ :-)
Pointy

2
@ Pointy ฉันดูผู้ใช้เป็นหลักเมื่อเป็นปลั๊กอินของบุคคลที่สาม และกระตุ้นให้ผู้คนนั่งลงและใช้มัน!
Raynos

1
._Deferredเป็นเพียง "วัตถุรอการตัดบัญชี" ที่แท้จริงซึ่ง.Deferredใช้ มันเป็นวัตถุภายในซึ่งคุณมักจะไม่ต้องการ
David Tang

คำตอบ:


212

กรณีการใช้งานที่ดีที่สุดที่ฉันคิดได้คือการแคชการตอบกลับ 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()จัดการไม่ใช่สัญญา / รอการตัดบัญชีเป็นเสร็จสมบูรณ์ทันทีดำเนินการใด ๆ.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
ตัวอย่างของ brilliants สองตัวอย่าง ฉันใช้บางสิ่งที่คล้ายกับอันที่ 2 แต่ด้วยคำขอ ajax 4 อันและทำงานได้ดีนอกจากจะอ่านได้ง่ายกระชับกะทัดรัดตรรกะบำรุงรักษาได้ ฯลฯ jQuery เลิกใช้แล้วเป็นสิ่งที่ดีจริง
PJP

5
นี่คือวิดีโอที่มีประโยชน์ในหัวข้อนี้bigbinary.com/videos/3-using-deferred-in-jquery
Nick Vanderbilt

5
การแคชจะไม่ทำงานหากผลลัพธ์นั้นมีค่าไม่แน่นอน นอกจากนี้ฉันไม่ชอบความจริงที่ getData คืนค่า 2 ชนิดขึ้นอยู่กับสาขาที่ถ่าย
Marko Dumic

3
ดูคำตอบของ Julian D. ด้านล่างเพื่อการใช้งานแคชที่ดีขึ้น
event_jr

1
ฉันไม่เข้าใจว่าตัวอย่างรหัสแรกทำงานอย่างไร: ฉันเข้าใจกรณีที่วัตถุไม่ได้ถูกแคช แต่ถ้าเป็นเช่นนั้นจะcache[ val ]ไม่คืนสัญญา (เอกสาร jquery บอกว่าพารามิเตอร์เป็นข้อมูลที่ส่งคืนโดยผู้ส่ง) หมายความว่า การเข้าถึงของสมาชิก.thenจะเกิดข้อผิดพลาด ... ใช่ไหม ฉันพลาดอะไรไป
chacham15

79

นี่คือการดำเนินงานที่แตกต่างกันเล็กน้อยของแคช AJAX เช่นเดียวกับในคำตอบของ ehynd

ดังที่ระบุไว้ในคำถามติดตามของ fortuneRice การใช้งานของ ehynd ไม่ได้ป้องกันคำขอที่เหมือนกันหลายคำขอหากมีการร้องขอก่อนที่จะส่งคืนหนึ่งคำขอ นั่นคือ,

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

จะส่งผลให้คำขอ AJAX 3 คำขอหากผลลัพธ์สำหรับ "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

การรอการตัดบัญชีสามารถใช้แทน mutex ได้ นี่เป็นสิ่งเดียวกับสถานการณ์การใช้งาน 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
});

เมื่อใช้ Deferred เป็น mutex เท่านั้นให้ระวังผลกระทบด้านประสิทธิภาพ (http://jsperf.com/deferred-vs-mutex/2) แม้ว่าความสะดวกสบายรวมถึงสิทธิประโยชน์เพิ่มเติมที่จัดทำโดย Deferred ก็คุ้มค่าและในการใช้งานจริง (อิงจากเหตุการณ์ที่ผู้ใช้ขับเคลื่อน) ผลกระทบต่อประสิทธิภาพไม่ควรสังเกตได้


เป็นเรื่องยากสำหรับฉันที่จะพบสิ่งนี้ ฉันใช้มันในฟังก์ชั่นที่มี setInterval ซึ่งจะคืนสัญญาที่ได้รับการแก้ไขและทำลายตัวเองเมื่อความกว้างของ div ทำให้มันเกินจำนวนที่แน่นอน สำหรับการแก้ไขปัญหาและการแก้ไขหากฉันไม่สามารถแก้ปัญหาของฉันได้ แต่ฉันมีความสุขกับมัน
JSG


20

การใช้งานอื่นที่ฉันตั้งใจไว้เพื่อจุดประสงค์ที่ดีคือการดึงข้อมูลจากหลาย ๆ แหล่ง ในตัวอย่างด้านล่างฉันกำลังดึงวัตถุสคีมา JSON อิสระหลายรายการที่ใช้ในแอปพลิเคชันที่มีอยู่เพื่อตรวจสอบความถูกต้องระหว่างไคลเอนต์และเซิร์ฟเวอร์ REST ในกรณีนี้ฉันไม่ต้องการให้แอปพลิเคชันฝั่งเบราว์เซอร์เริ่มโหลดข้อมูลก่อนที่จะโหลด schema ทั้งหมด $ .when.apply (). จากนั้น () เหมาะสำหรับสิ่งนี้ ขอบคุณ 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 เบราว์เซอร์ 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มันเป็นการหลอกลวงที่สมบูรณ์แบบ!
Raynos

เมื่อรหัสของขนาดของคุณซับซ้อนอัลกอริทึมการใช้งาน JavaScript ใน webkit จะหลวมประสานเมื่อมีการเรียกใช้ฟังก์ชั่นสำหรับแต่ละพิกเซลที่คุณปรับขนาดหน้าต่าง รอการตัดบัญชีทำให้การเรียกกลับของคุณอยู่ในคิวและดำเนินการตามลำดับ FIFO ดังนั้นหากคุณเพิ่มการโทรกลับ 'เสร็จสิ้น' และดำเนินการทันทีเนื่องจากการเลื่อนเวลาออกไปได้รับการแก้ไขแล้วการโทรกลับ 'เสร็จเรียบร้อย' อีกรายการที่เพิ่มลงในการเลื่อนออกไปในขณะที่การโทรกลับครั้งแรกยังคงทำงานอยู่จะถูกเพิ่มลงในคิว สำหรับการโทรกลับครั้งแรกเพื่อกลับ ฉันหวังว่านี้ตอบคำถามของคุณ.
MilošRašić

ล่าม JS ในเบราว์เซอร์เป็นเธรดเดียว เว้นแต่ resizeAlgorithm ของคุณมีรหัส async บางส่วนภายในนั้นฟังก์ชั่นทั้งควรมีการดำเนินงานที่เสร็จสมบูรณ์ก่อนที่จะโทรไปเพื่อ.doneจะทำ
Raynos

@ Raynos: ฉันรู้ว่า แต่ฉันพยายามที่จะเพียงแค่เรียก resizeAlgorithm ในการปรับขนาดและให้หน้าขาวว่างเปล่าในเบราว์เซอร์ webkit ในขณะที่ทำงานอย่างสมบูรณ์ในผู้อื่น รอการตัดบัญชีแก้ปัญหานี้ ฉันไม่มีเวลามากพอที่จะทำการวิจัยเชิงลึกเกี่ยวกับเรื่องนี้ อาจเป็นข้อผิดพลาดของ webkit ฉันไม่คิดว่าการเลื่อนเวลาตามที่ใช้ในตัวอย่างของฉันจะช่วยได้หาก resizeAlgorithm มีโค้ดอะซิงโครนัสบ้าง
MilošRašić

2
คุณไม่ควรใช้บางอย่างเช่นปลั๊กอินเค้น / debounce benalman.com/projects/jquery-throttle-debounce-pluginเพื่อป้องกันไม่ให้การทำงานของคุณเพิ่มขึ้นอีกหนึ่งครั้งต่อการปรับขนาด
wheresrhys

2

นอกจากนี้คุณยังสามารถรวมเข้ากับห้องสมุดบุคคลที่สามซึ่งใช้ JQuery

หนึ่งไลบรารีดังกล่าวคือ Backbone ซึ่งจริง ๆ แล้วจะสนับสนุน Deferred ในเวอร์ชันถัดไป


2
ใช้ในสถานที่ของread more here on my blogมันเป็นการฝึกฝนที่ดีกว่าและสามารถช่วยให้คุณประหยัดคำตอบจากการถูกสแปม (โดยไม่ตั้งใจ) :)
Lokesh Mehra

1

ฉันเพิ่งใช้ Deferred ในรหัสจริง ในโครงการjQuery Terminalฉันมีฟังก์ชั่น exec ที่คำสั่งการโทรที่กำหนดโดยผู้ใช้ (เช่นเขากำลังป้อนและกด Enter) ฉันได้เพิ่ม 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');
});

คำสั่งสามารถเรียกใช้รหัส async และผู้บริหารจำเป็นต้องเรียกใช้รหัสผู้ใช้ตามลำดับ 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.
});

คำตอบของ Julian D. จะทำงานได้ถูกต้องและเป็นทางออกที่ดีกว่า

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.