Javascript - 异步调用后同步

Posted

技术标签:

【中文标题】Javascript - 异步调用后同步【英文标题】:Javascript - synchronizing after asynchronous calls 【发布时间】:2010-10-25 17:58:38 【问题描述】:

我有一个 javascript 对象,它需要 2 次调用外部服务器来构建其内容并做任何有意义的事情。该对象的构建使得实例化它的实例将自动进行这两个调用。这两个调用共享一个通用的回调函数,该函数对返回的数据进行操作,然后调用另一个方法。问题是在两个方法都返回之前不应该调用下一个方法。这是我目前实现的代码:

foo.bar.Object = function() 
this.currentCallbacks = 0;
this.expectedCallbacks = 2;

this.function1 = function() 
    // do stuff
    var me = this;
    foo.bar.sendRequest(new RequestObject, function(resp) 
        me.commonCallback(resp);
        );
;

this.function2 = function() 
    // do stuff
    var me = this;
    foo.bar.sendRequest(new RequestObject, function(resp) 
        me.commonCallback(resp);
        );
;

this.commonCallback = function(resp) 
    this.currentCallbacks++;
    // do stuff
    if (this.currentCallbacks == this.expectedCallbacks) 
        // call new method
    
;

this.function1();
this.function2();

如您所见,我强制对象在两个调用都返回后继续使用一个简单的计数器来验证它们都已返回。这可行,但似乎是一个非常糟糕的实现。我现在只使用 Javascript 几个星期,想知道是否有更好的方法来做同样的事情,我还没有偶然发现。

感谢所有帮助。

【问题讨论】:

你的做法是正确的。我认为您当前的方法没有任何问题。每个实例都需要自己的计数器来知道它何时收到所有响应。这是我能想到的解决您问题的唯一方法。 【参考方案1】:

除非您愿意序列化 AJAX,否则我想不出其他方法来执行您的建议。话虽如此,我认为您所拥有的相当不错,但您可能需要稍微清理一下结构,以免在您创建的对象中乱扔初始化数据。

这是一个可能对您有所帮助的函数:

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);
    ;

这个函数就是所谓的高阶函数——一个将函数作为参数的函数。此特定函数返回一个函数,该函数在被调用number_of_calls_before_opening 次时调用传递的函数。例如:

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

你可以使用这个作为你的回调方法:

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

第二个回调,无论是哪个,都会确保调用method。但这会导致另一个问题:gate 函数在没有任何上下文的情况下调用传递的函数,这意味着 this 将引用全局对象,而不是您正在构造的对象。有几种方法可以解决这个问题:您可以通过将 this 别名为 meself 来关闭 this。或者您可以创建另一个执行此操作的高阶函数。

这是第一个案例的样子:

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);

在后一种情况下,另一个高阶函数如下所示:

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

此函数返回一个函数,该函数在传递的上下文中调用传递的函数。一个例子如下:

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

从角度来看,您的代码如下所示:

foo.bar = function() 
    var callback = gate(bind_context(this, this.method), 2);

    sendAjax(new Request(), callback);
    sendAjax(new Request(), callback);

无论如何,一旦您进行了这些重构,您将清除正在构造的对象,其中包含仅在初始化时需要的所有成员。

【讨论】:

【参考方案2】:

我可以添加Underscore.js has a nice little helper for this:

创建一个仅在第一次运行后才运行的函数版本 被称为count times。 对异步响应分组很有用, 您想确保所有异步调用都已完成的地方, 在继续之前

_.after(count, function)

_after 的代码(截至 1.5.0 版):

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

The license info(截至 1.5.0 版)

【讨论】:

_.after(count, _.bind(fun, this);)【参考方案3】:

除了拥有这个计数器之外,几乎没有其他方法了。另一种选择是使用对象 并为每个请求添加一个键,如果完成则将其删除。这样您就可以立即知道哪个返回了。但解决方案保持不变。

您可以稍微更改代码。如果在您的示例中,您只需要在 commonCallback 中调用另一个函数(我将其称为 otherFunction),那么您就不需要 commonCallback。为了保存上下文,您已经使用了闭包。而不是

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

你可以这样做

foo.bar.sendRequest(new RequestObject, function(resp) 
            --me.expectedCallbacks || me.otherFunction(resp);
            );

【讨论】:

【参考方案4】:

这是凯尔先生的一些好东西。

为了简单一点,我通常使用 Start 和 Done 函数。 - Start 函数采用将要执行的函数列表。 - Done 函数被您传递给 start 方法的函数的回调调用。 - 此外,您可以将函数或函数列表传递给将在最后一个回调完成时执行的 done 方法。

声明如下所示。

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]();
    

这是一个使用 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.*);

简而言之,我们在这里所做的是 - 将一组函数传递给 Start 函数 -Start函数调用数组中的函数并设置PendingRequests的数量 - 在待处理请求的回调中,我们调用 Done 函数 - Done 函数采用一组函数 -Done 函数减少 PendingRequests 计数器 - 如果它们不再是待处理的请求,我们调用传递给 Done 函数的函数

这是一个简单但实​​用的同步网络调用示例。我试图使用一个被广泛使用的例子,所以我使用了谷歌地图 api。我希望有人觉得这很有用。

【讨论】:

【参考方案5】:

另一种方法是通过计时器获得同步点。不漂亮,但好处是不用在回调中添加对下一个函数的调用。

这里的函数execute_jobs 是入口点。它需要一个数据列表同时执行。它首先将等待的作业数设置为list 的大小。然后它设置一个计时器来测试结束条件(数字下降到 0)。最后它为每个数据发送一个作业。每个作业都会将等待作业的数量减少一个。

看起来像这样:

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]));
    

要改进此代码,您可以创建 JobJobList 类。 Job 将执行回调并减少待处理作业的数量,而 JobList 将聚合计时器并在作业完成后调用回调到下一个操作。

【讨论】:

【参考方案6】:

我也有同样的挫败感。当我链接更多异步调用时,它变成了回调地狱。所以,我想出了自己的解决方案。我确信那里有类似的解决方案,但我想创建一些非常简单易用的东西。 Asynq 是我编写的用于链接异步任务的脚本。所以要在 f1 之后运行 f2,你可以这样做:

asynq.run(f1, f2)

您可以链接任意数量的函数。您还可以指定参数或对数组中的元素运行一系列任务。我希望这个库可以解决您的问题或其他人遇到的类似问题。

【讨论】:

以上是关于Javascript - 异步调用后同步的主要内容,如果未能解决你的问题,请参考以下文章

javascript中事件处理程序的异步或同步调用

javascript 循环中调用异步的同步需求

如何在同步函数中等待 JavaScript 中的异步调用?

进阶学习5:JavaScript异步编程——同步模式异步模式调用栈工作线程消息队列事件循环回调函数

浏览器中的JavaScript事件循环机制

C# 同步调用 异步调用 异步回调 多线程的作用