Javascript - 异步调用后同步

时间:2009-05-13 03:23:14

标签: javascript asynchronous callback synchronous

我有一个Javascript对象,需要2次调用外部服务器来构建其内容并执行任何有意义的操作。构建对象使得实例化它的实例将自动进行这两个调用。 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几周了,我想知道是否有更好的方法来做同样的事情,我还没有偶然发现。

感谢您的帮助。

6 个答案:

答案 0 :(得分:13)

除非你愿意序列化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将引用全局对象,而不是您正在构造的对象。有几种方法可以解决这个问题:您可以通过将其别名化为thisme来关闭self。或者您可以创建另一个更高阶函数来完成此任务。

以下是第一种情况:

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

在任何情况下,一旦你完成了这些重构,你就会清除正在构建的对象,而该对象只是初始化所需的所有成员。

答案 1 :(得分:8)

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

  

创建仅在首先运行的函数版本   被称为计数时间。 用于对异步响应进行分组,   你想确定所有异步调用已经完成的地方,   在继续之前

_.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)

答案 2 :(得分:5)

除了拥有这个计数器之外别无他法。另一种选择是使用对象{}并为每个请求添加一个键,如果完成则将其删除。通过这种方式,您可以立即知道返回的内容。但解决方案保持不变。

您可以稍微更改一下代码。如果在你的例子中你只需要调用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);
            });

答案 3 :(得分:2)

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

为了简单起见,我通常使用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.*);
}

简而言之,我们在这里做的是什么 - 将一系列功能传递给开始功能
- 开始函数调用数组中的函数并设置 PendingRequests 的数量
- 在我们待处理请求的回调中,我们称之为完成功能 - 完成功能需要一系列功能
- 完成功能减少PendingRequests计数器
- 如果他们没有待处理请求,我们将传递给完成功能的函数调用

这是一个简单但实​​际的Web调用同步示例。我试着用一个广泛使用的东西的例子,所以我选择了谷歌地图api。我希望有人觉得这很有用。

答案 4 :(得分:0)

另一种方法是通过计时器获得同步点。它不漂亮,但它的优点是不必将调用添加到回调中的下一个函数。

这里函数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将聚合计时器并在作业完成后将回调调用到下一个操作。

答案 5 :(得分:-1)

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

asynq.run(f1,f2)

您可以根据需要链接任意数量的功能。您还可以在数组中的元素上指定参数或运行一系列任务。我希望这个图书馆可以解决您的问题或其他人遇到的类似问题。