Javascript - synchronizing after asynchronous calls

前端 未结 6 1623
北恋
北恋 2021-02-04 02:08

I have a Javascript object that requires 2 calls out to an external server to build its contents and do anything meaningful. The object is built such that instantiating an inst

相关标签:
6条回答
  • 2021-02-04 02:40

    That's some good stuff Mr. Kyle.

    To put it a bit simpler, I usually use a Start and a Done function.
    -The Start function takes a list of functions that will be executed.
    -The Done function gets called by the callbacks of your functions that you passed to the start method.
    -Additionally, you can pass a function, or list of functions to the done method that will be executed when the last callback completes.

    The declarations look like this.

    var PendingRequests = 0;
    function Start(Requests) {
        PendingRequests = Requests.length;
        for (var i = 0; i < Requests.length; i++)
            Requests[i]();
    };
    //Called when async responses complete. 
    function Done(CompletedEvents) {
    PendingRequests--;
        if (PendingRequests == 0) {
            for (var i = 0; i < CompletedEvents.length; i++)
                CompletedEvents[i]();
        }
    }
    

    Here's a simple example using the google maps api.

    //Variables
    var originAddress = "*Some address/zip code here*"; //Location A
    var formattedAddress; //Formatted address of Location B
    var distance; //Distance between A and B
    var location; //Location B
    
    //This is the start function above. Passing an array of two functions defined below.
    Start(new Array(GetPlaceDetails, GetDistances));
    
    
    //This function makes a request to get detailed information on a place. 
    //Then callsback with the **GetPlaceDetailsComplete** function
    function GetPlaceDetails() {
        var request = {
            reference: location.reference //Google maps reference id
        };
        var PlacesService = new google.maps.places.PlacesService(Map);
        PlacesService.getDetails(request, GetPlaceDetailsComplete);
    }
    
    function GetPlaceDetailsComplete(place, status) {
        if (status == google.maps.places.PlacesServiceStatus.OK) {
            formattedAddress = place.formatted_address;
            Done(new Array(PrintDetails));
        }
    }
    
    
    function GetDistances() {
        distService = new google.maps.DistanceMatrixService();
        distService.getDistanceMatrix(
        {
            origins: originAddress, 
            destinations: [location.geometry.location], //Location contains lat and lng
            travelMode: google.maps.TravelMode.DRIVING,
            unitSystem: google.maps.UnitSystem.IMPERIAL,
            avoidHighways: false,
            avoidTolls: false
        }, GetDistancesComplete);
    }
    function GetDistancesComplete(results, status) {
        if (status == google.maps.DistanceMatrixStatus.OK) {
            distance = results[0].distance.text;
            Done(new Array(PrintDetails));
        }
    }
    
    function PrintDetails() {
        alert(*Whatever you feel like printing.*);
    }
    

    So in a nutshell, what we're doing here is
    -Passing an array of functions to the Start function
    -The Start function calls the functions in the array and sets the number of PendingRequests
    -In the callbacks for our pending requests, we call the Done function -The Done function takes an array of functions
    -The Done function decrements the PendingRequests counter
    -If their are no more pending requests, we call the functions passed to the Done function

    That's a simple, but practicle example of sychronizing web calls. I tried to use an example of something that's widely used, so I went with the Google maps api. I hope someone finds this useful.

    0 讨论(0)
  • 2021-02-04 02:48

    Unless you're willing to serialize the AJAX there is no other way that I can think of to do what you're proposing. That being said, I think what you have is fairly good, but you might want to clean up the structure a bit to not litter the object you're creating with initialization data.

    Here is a function that might help you:

    function gate(fn, number_of_calls_before_opening) {
        return function() {
            arguments.callee._call_count = (arguments.callee._call_count || 0) + 1;
            if (arguments.callee._call_count >= number_of_calls_before_opening)
                fn.apply(null, arguments);
        };
    }
    

    This function is what's known as a higher-order function - a function that takes functions as arguments. This particular function returns a function that calls the passed function when it has been called number_of_calls_before_opening times. For example:

    var f = gate(function(arg) { alert(arg); }, 2);
    f('hello');
    f('world'); // An alert will popup for this call.
    

    You could make use of this as your callback method:

    foo.bar = function() {
        var callback = gate(this.method, 2);
        sendAjax(new Request(), callback);
        sendAjax(new Request(), callback);
    }
    

    The second callback, whichever it is will ensure that method is called. But this leads to another problem: the gate function calls the passed function without any context, meaning this will refer to the global object, not the object that you are constructing. There are several ways to get around this: You can either close-over this by aliasing it to me or self. Or you can create another higher order function that does just that.

    Here's what the first case would look like:

    foo.bar = function() {
        var me = this;        
        var callback = gate(function(a,b,c) { me.method(a,b,c); }, 2);
    
        sendAjax(new Request(), callback);
        sendAjax(new Request(), callback);
    }
    

    In the latter case, the other higher order function would be something like the following:

    function bind_context(context, fn) {
        return function() {
            return fn.apply(context, arguments);
        };
    }
    

    This function returns a function that calls the passed function in the passed context. An example of it would be as follows:

    var obj = {};
    var func = function(name) { this.name = name; };
    var method = bind_context(obj, func);
    method('Your Name!');
    alert(obj.name); // Your Name!
    

    To put it in perspective, your code would look as follows:

    foo.bar = function() {
        var callback = gate(bind_context(this, this.method), 2);
    
        sendAjax(new Request(), callback);
        sendAjax(new Request(), callback);
    }
    

    In any case, once you've made these refactorings you will have cleared up the object being constructed of all its members that are only needed for initialization.

    0 讨论(0)
  • 2021-02-04 02:55

    I can add that Underscore.js has a nice little helper for this:

    Creates a version of the function that will only be run after first being called count times. Useful for grouping asynchronous responses, where you want to be sure that all the async calls have finished, before proceeding.

    _.after(count, function)
    

    The code for _after (as-of version 1.5.0):

    _.after = function(times, func) {
      return function() {
        if (--times < 1) {
          return func.apply(this, arguments);
        }
      };
    };
    

    The license info (as-of version 1.5.0)

    0 讨论(0)
  • 2021-02-04 02:56

    Another way would be to have a sync point thanks to a timer. It is not beautiful, but it has the advantage of not having to add the call to the next function inside the callback.

    Here the function execute_jobs is the entry point. it take a list of data to execute simultaneously. It first sets the number of jobs to wait to the size of the list. Then it set a timer to test for the end condition (the number falling down to 0). And finally it sends a job for each data. Each job decrease the number of awaited jobs by one.

    It would look like something like that:

    var g_numJobs = 0;
    
    function async_task(data) {
        //
        // ... execute the task on the data ...
        //
    
        // Decrease the number of jobs left to execute.
        --g_numJobs;
    }
    
    function execute_jobs(list) {
        // Set the number of jobs we want to wait for.
        g_numJobs = list.length;
    
        // Set the timer (test every 50ms).
        var timer = setInterval(function() {
            if(g_numJobs == 0) {
                clearInterval(timer);
                do_next_action();
            }
        }, 50);
    
        // Send the jobs.
        for(var i = 0; i < list.length; ++i) {
            async_task(list[i]));
        }
    }
    

    To improve this code you can do a Job and JobList classes. The Job would execute a callback and decrease the number of pending jobs, while the JobList would aggregate the timer and call the callback to the next action once the jobs are finished.

    0 讨论(0)
  • 2021-02-04 02:59

    There is barely another way than to have this counter. Another option would be to use an object {} and add a key for every request and remove it if finished. This way you would know immediately which has returned. But the solution stays the same.

    You can change the code a little bit. If it is like in your example that you only need to call another function inside of commonCallback (I called it otherFunction) than you don't need the commonCallback. In order to save the context you did use closures already. Instead of

    foo.bar.sendRequest(new RequestObject, function(resp) {
                me.commonCallback(resp);
                });
    

    you could do it this way

    foo.bar.sendRequest(new RequestObject, function(resp) {
                --me.expectedCallbacks || me.otherFunction(resp);
                });
    
    0 讨论(0)
  • 2021-02-04 03:03

    I shared the same frustration. As I chained more asynchronous calls, it became a callback hell. So, I came up with my own solution. I'm sure there are similar solutions out there, but I wanted to create something very simple and easy to use. Asynq is a script that I wrote to chain asynchronous tasks. So to run f2 after f1, you can do:

    asynq.run(f1, f2)

    You can chain as many functions as you want. You can also specify parameters or run a series of tasks on elements in an array too. I hope this library can solve your issues or similar issues others are having.

    0 讨论(0)
提交回复
热议问题