Async, Callbacks, Closure, Oh My

2020-07-17 15:39发布

问题:

Here's what I'm trying to do, and failing miserably at:

I have a MongoDB filled with GPS readings from a device attached to a car. The readings are timestamped and have a lat/lng.

For each reading, I want to create a Position class that contains location, timestamp and some other information.

To plot this on Google Maps, I want to create a Route class, which is made up of an array of Positions Objects.

The main function at the top loads a route from a sample JSON file (to avoid CSRF problems... another story), creates the Position objects and route object fine. The problems start when I try to plot the route.

var positions = this.getPositions();

If I type

console.log(positions);

right below that line, it will print the positions array fine. It takes a bit to do that since it's large, but it works.

If however, I type

console.log(positions[0]);

it wont work because the positions haven't loaded yet.

What do I do?

I'm using classy.js from http://classy.pocoo.org but I've verified this isn't a problem with that.

function main()
{
    var route = Route('20120928_025650_0');
    route.plot();
}
var Route = Class.$extend({
    __init__ : function(imaging_run_id)  {
        this.imaging_run_id = imaging_run_id;
        this.positions = [];
        this.load();
    },

    getPositions : function() {
        return (this.positions);
    },

    load : function() {
        //var url='http://localhost:5001/gps/by_imaging_run_id/' + this.imaging_run_id;
        var test_url = '/static/20120928_025650_0.json';
        var me = this;
        $.getJSON(test_url, function(route) {
            for(var position in route) {
                var obj = route[position];
                var new_position = Position(obj['_id'], obj['lat'], obj['lng'], obj['timestamp'], obj['epoch_timestamp'], obj['is_valid']);
                me.pushPositions(new_position);
            }
            me.orderPositions();
        })
        .error(function(jqXhr, textStatus, error) {
            alert("ERROR: " + textStatus + ", " + error);
        });
    },

    orderPositions : function() {
        var unsorted_array = this.getPositions();
        var sorted = unsorted_array.sort(function(a,b){ //Custom sort function
            return a['timestamp'] - b['timestamp']; //Sort ascending
        });
        this.setPositions(sorted);
    },

    plot : function() {
        var positions = this.getPositions();
        var points = [];
        var bounds = new google.maps.LatLngBounds();
        for(var i=0; i<positions.length; i++)
        {
            var obj = positions[i];
            var point = new google.maps.LatLng(obj['location'][0], obj['location'][1]);
            points.push(point);
            bounds.extend(point);
        }
        // Create the polyline.
        var route = new google.maps.Polyline({
            path: points,
            strokeColor: '#e81971',
            strokeOpacity: 1.0,
            strokeWeight: 4
        });
        map.fitBounds(bounds);
        route.setMap(map);
    }
});

var Position = Class.$extend({
    __init__ : function(id, lat, lng, timestamp, epoch_timestamp, valid)  {
        this.id = id;
        this.location = [lat, lng];
        this.timestamp = new Date(timestamp);
        this.epoch_timestamp = new Date(epoch_timestamp);
        this.valid = valid;
        this.anchor_location;
    },.....

回答1:

If I understand it correctly, you will want to do something like this:

var positions = this.getPositions(function(positions) {
    console.log(positions[0]);
});

That is, you'll want to write "getPositions" in a way that it accepts a single callback parameter which is invoked once the positions have loaded successfully, and is passed the positions array. In getPositions you could check if the positions have already been loaded, and if so, directly invoke the callback. Otherwise, you'll add them to a queue of callbacks (e.g. this.positionsLoadedCallbacks), through which you iterate after all positions have been loaded (I think that would be somewhere in your load function near me.orderPositions()).

For example, your getPositions function might look like this:

getPositions : function(callback) {
    if(this.positions !== null) {
        callback(this.positions);
        return;
    } 

    this.positionsLoadedCallbacks.push(callback);
},

Somewhere after you are sure the positions have been loaded (i.e. in the loadJSON success callback) you'll need to put something like this:

for(var i=0; i < this.positionsLoadedCallbacks.length; i++) {
    this.positionsLoadedCallbacks[i](this.positions);
}

And don't forget to initialize this.positionsLoadedCallbacks :)

console.log trivia

The reason that console.log(positions) works and console.log(positions[0]) does not is easy: if you pass an object reference to console.log, the object will be inspected when you click on the little "expand" arrow and try to take a look inside the object/array. By the time you click that arrow, the positions have been loaded, of course. However, if you pass a specific array element (e.g. positions[0]) it will directly try to look that value up, find that it is still undefined, and record that result in the console.

Try it yourself:

var i = [];
console.log([i]);
i.push(123);

The previous snippet, in chrome 24, shows [Array[0]] in the console, but when I expand it, it tells me that the array as length: 1 and that its first element is 123