简体   繁体   中英

Scoping a variable shared by multiple async callbacks

I'm using the async.js library by Caolan McMahon and the jQueryUI progress bar to provide feedback to the user while several async calls gather data and fill in elements of a complex graph.

My question is: What's the best way to scope data that needs to be shared by the asynchronous methods?

This is a simplified example of what I'm doing. I've got it working using global variables but they disturb me a little and make jsLint complain. Passing arguments or scoping at the Document Ready function breaks it.

The counterparts to updateA() et. al. in my real code are hundreds of lines and include XHR calls.

JavaScript:

// global variables.  Bad?
var steps = 3;
var ticked = 0;
var otherCounter = 0;

$(function() {
    $('#progressbar').progressbar({
        value: 0
    });

    async.parallel([
        function(onDoneCallback) {
        updateA(onDoneCallback);},
        function(onDoneCallback) {
        updateB(onDoneCallback);},
        function(onDoneCallback) {
        updateC(onDoneCallback);}
    ], function(err, results) { // final callback when they're all done
        tickProgress('All done after ' + ticked + ' ticks.', true);
    });
});

function tickProgress(message) {
    var curvalue = $('#progressbar').progressbar('option', 'value');
    var done = false;

    if (arguments.length > 1) {
        done = arguments[1];
    }

    $('#progress_text').html(message);

    if (done) {
        $('#progressbar').progressbar('option', 'value', 100);
    }
    else {
        $('#progressbar').progressbar('option', 'value', curvalue + 100 / steps);
    }

    ticked++; // global OK here?
}

function updateA(onDoneCallback) {
    setTimeout(function() {
        $('#a').html('A is foo. otherCounter ' + otherCounter);
        tickProgress('updated A at otherCounter ' + otherCounter);
        otherCounter++;
        onDoneCallback(null, 'A done');
    }, 1000);

}

function updateB(onDoneCallback) {
    setTimeout(function() {
        $('#b').html('B is bottle. otherCounter ' + otherCounter);
        tickProgress('updated B at otherCounter ' + otherCounter);
        otherCounter++;
        onDoneCallback(null, 'B is OK');
    }, 100);
}

function updateC(onDoneCallback) {
    setTimeout(function() {
        $('#c').html('C is cauliflower. otherCounter ' + otherCounter);
        tickProgress('updated C at otherCounter ' + otherCounter);
        otherCounter++;
        onDoneCallback(null, 'C done');
    }, 2000);
}

HTML:

<p id="progress_text" style="background:yellow">Loading...</p>
<div id="progressbar"></div>
<hr />
<h2>a</h2>
<p id="a">Looking up a...</p>

<h2>b</h2>
<p id="b">Looking up b...</p>

<h2>c</h2>
<p id="c">Looking up c...</p>

Fiddle:

I've got the sample code at JSFiddle if you want to bang on it there.

In generall, it's always a great idea to create your own closured Function-Context'ed "region". You can do that by wrapping a self invoking anonymous function around your application. This could look like

(function(window, document, $) {
     // all your app logic goes into here
     var steps = 3;
     var ticked = 0;
     var otherCounter = 0;

     // ...
}(this, this.document, jQuery))

That way, you never clobber the global namespace. Of course you need to have a global object sometimes, but you really should try to avoid that unless absolutly necessary.

You could put the global vars into a "state" object and pass it to all the callback functions. eg

$(function() {
    var progressState = {
        steps: 3,
        ticked: 0,
        otherCounter: 0
    };

    $('#progressbar').progressbar({value: 0});

    async.parallel(
      [
        function(onDoneCallback) {updateA(onDoneCallback, progressState);},
        function(onDoneCallback) {updateB(onDoneCallback, progressState);},
        function(onDoneCallback) {updateC(onDoneCallback, progressState);}
      ],
      function(err, results) { // final callback when they're all done
        tickProgress('All done after ' + progressState.ticked + ' ticks.', true, progressState);
      }
    );
});

You would then change updateA, updateB, updateC and tickProgress to use the provided state object, instead of the global vars.

david turned me on to this approach the other day in the JS chat room. See it work at jsfiddle.

It makes use of a global object, but I like the logical encapsulation of the methods within the object better than the suggestions by jAndy and kirilloid .

var Updater = (function() {
    var steps = 3;
    var ticked = 0;
    var otherCounter = 0;

    function a(onDoneCallback) {
        setTimeout(function() {
            $('#a').html('A is foo. otherCounter ' + otherCounter);
            tickProgress('updated A at otherCounter ' + otherCounter);
            otherCounter++;
            onDoneCallback(null, 'A done');
        }, 1000);

    }

    function b(onDoneCallback) {
        setTimeout(function() {
            $('#b').html('B is bottle. otherCounter ' + otherCounter);
            tickProgress('updated B at otherCounter ' + otherCounter);
            otherCounter++;
            onDoneCallback(null, 'B is OK');
        }, 100);
    }

    function c(onDoneCallback) {
        setTimeout(function() {
            $('#c').html('C is cauliflower. otherCounter ' + otherCounter);
            tickProgress('updated C at otherCounter ' + otherCounter);
            otherCounter++;
            onDoneCallback(null, 'C done');
        }, 2000);
    }

    function tickProgress(message) {
        var curvalue = $('#progressbar').progressbar('option', 'value');
        var done = false;

        if (arguments.length > 1) {
            done = arguments[1];
        }

        $('#progress_text').html(message);

        if (done) {
            $('#progressbar').progressbar('option', 'value', 100);
        }
        else {
            $('#progressbar').progressbar('option', 'value', curvalue + 100 / Updater.getSteps());
        }
        Updater.tick(); // global OK here?
    }

    return {
        a: a,
        b: b,
        c: c,
        tickProgress: tickProgress,
        tick: function() {
            ticked++;
        },
        getTicks: function() {
            return ticked;
        },
        getSteps: function() {
            return steps;
        }
    };
}());

$(function() {
    $('#progressbar').progressbar({
        value: 0
    });

    async.parallel([
        function(onDoneCallback) {
        Updater.a(onDoneCallback);},
        function(onDoneCallback) {
        Updater.b(onDoneCallback);},
        function(onDoneCallback) {
        Updater.c(onDoneCallback);}
    ], function(err, results) { // final callback when they're all done
        Updater.tickProgress('All done after ' + Updater.getTicks() + ' ticks.', true);
    });
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM