20

我有一个 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 几个星期,想知道是否有更好的方法来做同样的事情,我还没有偶然发现。

感谢您的任何帮助。

4

6 回答 6

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将引用全局对象,而不是您正在构造的对象。有几种方法可以解决这个问题:您可以this通过将其别名为me或来关闭它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);
}

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

于 2009-05-13T04:30:00.097 回答
8

我可以补充一点,Underscore.js 有一个很好的小帮手

创建仅在第一次调用 count 次后才运行的函数版本。对于分组异步响应很有用,您希望在继续之前确保所有异步调用都已完成

_.after(count, function)

_after(截至版本 1.5.0)的代码:

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

许可证信息(截至 1.5.0 版)

于 2011-12-20T10:44:29.220 回答
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);
            });
于 2009-05-13T05:58:59.797 回答
2

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

为了简单一点,我通常使用 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。我希望有人觉得这很有用。

于 2012-04-12T17:03:01.290 回答
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将聚合计时器并在作业完成后将回调调用到下一个操作。

于 2013-06-13T03:19:27.230 回答
-1

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

异步运行(f1,f2)

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

于 2014-04-04T01:01:06.567 回答