13

我已经用 JavaScript 开发了相当长的一段时间,但我还是一名牛仔开发人员,因为一直困扰着我的许多事情之一就是同步 JavaScript 的回调。

当这个问题出现时,我将描述一个通用场景:我有一堆操作要通过 for 循环执行多次,并且每个操作都有一个回调。在 for 循环之后,我需要执行另一个操作,但这个操作只有在 for 循环的所有回调都完成后才能成功执行。

代码示例:

 for ... in ... {
   myFunc1(callback); // callbacks are executed asynchly
 }

 myFunc2(); // can only execute properly if all the myFunc1 callbacks are done

建议的解决方案:

在循环开始时启动一个计数器,保存循环的长度,每个回调递减该计数器。当计数器为 0 时,执行 myFunc2。这本质上是为了让回调知道它是否是序列中的最后一个回调,如果是,则在完成后调用 myFunc2。

问题:

  1. 代码中的每个这样的序列都需要一个计数器,并且到处都有无意义的计数器不是一个好习惯。
  2. 如果您还记得经典同步问题中的线程冲突是如何发生的,当多个线程都在同一个 var 上调用 var-- 时,会出现不良结果。JavaScript 中也会发生同样的情况吗?

终极问题:

有更好的解决方案吗?

4

6 回答 6

13

好消息是 JavaScript 是单线程的。这意味着解决方案通常适用于“共享”变量,即不需要互斥锁。

如果你想序列化异步任务,然后是完成回调,你可以使用这个辅助函数:

function serializeTasks(arr, fn, done)
{
    var current = 0;

    fn(function iterate() {
        if (++current < arr.length) {
            fn(iterate, arr[current]);
        } else {
            done();
        }
    }, arr[current]);
}

第一个参数是需要在每次传递中传递的值数组,第二个参数是循环回调(解释如下),最后一个参数是完成回调函数。

这是循环回调函数:

function loopFn(nextTask, value) {
    myFunc1(value, nextTask);
}

传递的第一个参数是一个将执行下一个任务的函数,它被传递给你的异步函数。第二个参数是值数组的当前条目。

假设异步任务如下所示:

function myFunc1(value, callback)
{
  console.log(value);
  callback();
}

它打印值,然后调用回调;简单的。

然后,让整个事情动起来:

serializeTasks([1,2, 3], loopFn, function() {
    console.log('done');
});

演示

要并行化它们,您需要一个不同的函数:

function parallelizeTasks(arr, fn, done)
{
    var total = arr.length,
    doneTask = function() {
      if (--total === 0) {
        done();
      }
    };

    arr.forEach(function(value) {
      fn(doneTask, value);
    });
}

您的循环函数将是这样的(仅参数名称更改):

function loopFn(doneTask, value) {
    myFunc1(value, doneTask);
}

演示

于 2013-04-12T06:33:39.220 回答
3

第二个问题并不是真正的问题,只要其中每一个都在一个单独的函数中并且变量被正确声明(使用var);函数中的局部变量不会相互干扰。

第一个问题有点问题。其他人也很生气,最终制作了库来为您包装这种模式。我喜欢async。有了它,您的代码可能如下所示:

async.each(someArray, myFunc1, myFunc2);

它还提供了许多其他异步构建块。如果你正在做很多异步的事情,我建议你看看它。

于 2013-04-12T05:21:22.667 回答
2

您可以通过使用 jQuery 延迟对象来实现这一点。

var deferred = $.Deferred();
var success = function () {
    // resolve the deferred with your object as the data
    deferred.resolve({
        result:...;
    });
};
于 2013-04-12T05:27:35.683 回答
1

单线程并不总是得到保证。不要误会。

案例 1:例如,如果我们有 2 个函数,如下所示。

var count=0;
function1(){
  alert("this thread will be suspended, count:"+count);
}
function2(){
  //anything
  count++;
  dump(count+"\n");
}

那么在function1返回之前,function2也会被调用,如果保证1个线程,那么function2在function1返回之前不会被调用。你可以试试这个。当您收到警报时,您会发现计数正在上升。

案例2:使用Firefox,chrome code,在1个函数返回之前(里面没有alert),也可以调用另一个函数。

所以确实需要互斥锁。

于 2014-07-10T02:46:00.880 回答
1

使用此辅助功能:

function afterAll(callback,what) {
  what.counter = (what.counter || 0) + 1;
  return function() {
    callback(); 
    if(--what.counter == 0) 
      what();
  };
}

您的循环将如下所示:

function whenAllDone() { ... }
for (... in ...) {
  myFunc1(afterAll(callback,whenAllDone)); 
}

这里afterAll为回调创建代理函数,它也递减计数器。并在所有回调完成时调用 whenAllDone 函数。

于 2013-04-12T06:24:18.030 回答
0

有很多很多方法可以实现这一点,希望这些建议对您有所帮助!

首先,我会将回调转换为承诺!这是一种方法:

function aPromise(arg) {
    return new Promise((resolve, reject) => {
        aCallback(arg, (err, result) => {
            if(err) reject(err);
            else resolve(result);
        });
    })
}

接下来,使用reduce对数组的元素进行一一处理!

const arrayOfArg = ["one", "two", "three"];
const promise = arrayOfArg.reduce(
    (promise, arg) => promise.then(() => aPromise(arg)), // after the previous promise, return the result of the aPromise function as the next promise
    Promise.resolve(null) // initial resolved promise
    );
promise.then(() => {
    // carry on
});

如果要同时处理数组的所有元素,请使用 map an Promise.all!

const arrayOfArg = ["one", "two", "three"];
const promise = Promise.all(arrayOfArg.map(
    arg => aPromise(arg)
));
promise.then(() => {
    // carry on
});

如果您能够使用 async / await 那么您可以简单地这样做:

const arrayOfArg = ["one", "two", "three"];
for(let arg of arrayOfArg) {
    await aPromise(arg); // wow
}

// carry on

你甚至可以像这样使用我非常酷的同步异步库:

const arrayOfArg = ["one", "two", "three"];
const context = {}; // can be any kind of object, this is the threadish context

for(let arg of arrayOfArg) {
    synchronizeCall(aPromise, arg); // synchronize the calls in the given context
}

join(context).then(() => { // join will resolve when all calls in the context are finshed
    // carry on
});

最后但并非最不重要的一点是,如果您真的不想使用Promise ,请使用精细的异步库。

const arrayOfArg = ["one", "two", "three"];
async.each(arrayOfArg, aCallback, err => {
    if(err) throw err; // handle the error!
    // carry on
});
于 2017-03-02T20:59:11.077 回答