如何同步JavaScript回调?

如何同步JavaScript回调?,javascript,multithreading,callback,synchronization,Javascript,Multithreading,Callback,Synchronization,我已经用JavaScript开发了很长一段时间了,但是net还是一个牛仔开发人员,因为我经常遇到的事情之一就是同步JavaScript的回调 当这个问题被提出时,我将描述一个通用场景:我有一堆操作要通过for循环执行多次,每个操作都有一个回调。在for循环之后,我需要执行另一个操作,但是这个操作只有在for循环的所有回调都完成后才能成功执行 代码示例: for ... in ... { myFunc1(callback); // callbacks are executed asynch

我已经用JavaScript开发了很长一段时间了,但是net还是一个牛仔开发人员,因为我经常遇到的事情之一就是同步JavaScript的回调

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

代码示例:

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

 myFunc2(); // can only execute properly if all the myFunc1 callbacks are done
建议的解决方案:

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

 myFunc2(); // can only execute properly if all the myFunc1 callbacks are done
在循环的开始处启动一个计数器,保存循环的长度,每个回调将递减该计数器。当计数器达到0时,执行myFunc2。这本质上是让回调知道它是否是序列中的最后一个回调,如果是,则在完成后调用myFunc2

问题:

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

 myFunc2(); // can only execute properly if all the myFunc1 callbacks are done
  • 代码中的每一个这样的序列都需要一个计数器,而到处都有无意义的计数器并不是一个好的做法
  • 如果您还记得经典同步问题中线程是如何冲突的,当多个线程都在同一个var上调用var时,就会出现不希望出现的结果。JavaScript中也会发生同样的情况吗
  • 终极问题:

     for ... in ... {
       myFunc1(callback); // callbacks are executed asynchly
     }
    
     myFunc2(); // can only execute properly if all the myFunc1 callbacks are done
    

    有更好的解决方案吗?

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

    第一个问题更像是一个问题。其他人也很恼火,最终制作了一些库来为您包装这种模式。我喜欢。使用它,您的代码可能如下所示:

    async.each(someArray, myFunc1, myFunc2);
    
    function whenAllDone() { ... }
    for (... in ...) {
      myFunc1(afterAll(callback,whenAllDone)); 
    }
    
    function myFunc1(value, callback)
    {
      console.log(value);
      callback();
    }
    
    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
    });
    

    它还提供了许多其他异步构建块。如果你正在做大量的异步工作,我建议你看看它。

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

    var deferred = $.Deferred();
    var success = function () {
        // resolve the deferred with your object as the data
        deferred.resolve({
            result:...;
        });
    };
    

    使用此辅助功能:

    function afterAll(callback,what) {
      what.counter = (what.counter || 0) + 1;
      return function() {
        callback(); 
        if(--what.counter == 0) 
          what();
      };
    }
    
    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 parallelizeTasks(arr, fn, done)
    {
        var total = arr.length,
        doneTask = function() {
          if (--total === 0) {
            done();
          }
        };
    
        arr.forEach(function(value) {
          fn(doneTask, value);
        });
    }
    
    您的循环将如下所示:

    async.each(someArray, myFunc1, myFunc2);
    
    function whenAllDone() { ... }
    for (... in ...) {
      myFunc1(afterAll(callback,whenAllDone)); 
    }
    
    function myFunc1(value, callback)
    {
      console.log(value);
      callback();
    }
    
    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
    });
    

    在这里
    毕竟
    为回调创建了代理函数,它还递减计数器。并在所有回调完成时调用whenaldone函数。

    好消息是JavaScript是单线程的;这意味着解决方案通常可以很好地处理“共享”变量,即不需要互斥锁

    如果要序列化异步任务,然后执行完成回调,则可以使用以下帮助函数:

    function afterAll(callback,what) {
      what.counter = (what.counter || 0) + 1;
      return function() {
        callback(); 
        if(--what.counter == 0) 
          what();
      };
    }
    
    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 parallelizeTasks(arr, fn, done)
    {
        var total = arr.length,
        doneTask = function() {
          if (--total === 0) {
            done();
          }
        };
    
        arr.forEach(function(value) {
          fn(doneTask, value);
        });
    }
    
    传递的第一个参数是将执行下一个任务的函数,它将传递给异步函数。第二个参数是值数组的当前条目

    假设异步任务如下所示:

    async.each(someArray, myFunc1, myFunc2);
    
    function whenAllDone() { ... }
    for (... in ...) {
      myFunc1(afterAll(callback,whenAllDone)); 
    }
    
    function myFunc1(value, callback)
    {
      console.log(value);
      callback();
    }
    
    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
    });
    
    它打印值,然后调用回调;简单

    然后,要启动整个过程:

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

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

    function afterAll(callback,what) {
      what.counter = (what.counter || 0) + 1;
      return function() {
        callback(); 
        if(--what.counter == 0) 
          what();
      };
    }
    
    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 parallelizeTasks(arr, fn, done)
    {
        var total = arr.length,
        doneTask = function() {
          if (--total === 0) {
            done();
          }
        };
    
        arr.forEach(function(value) {
          fn(doneTask, value);
        });
    }
    
    循环函数将如下所示(仅参数名称更改):


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

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

    var count=0;
    function1(){
      alert("this thread will be suspended, count:"+count);
    }
    function2(){
      //anything
      count++;
      dump(count+"\n");
    }
    
    然后在function1返回之前,也会调用function2,如果保证有1个线程,那么在function1返回之前不会调用function2。你可以试试这个。当你被提醒时,你会发现计数在上升

    案例2:对于Firefox,chrome代码,在1个函数返回之前(内部没有警报),还可以调用另一个函数


    因此,确实需要一个互斥锁。

    实现这一点的方法很多,我希望这些建议能有所帮助

    首先,我要将回调转换为承诺!有一种方法可以做到这一点:

    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
    
    您甚至可以像这样使用我非常酷的库:

    async.each(someArray, myFunc1, myFunc2);
    
    function whenAllDone() { ... }
    for (... in ...) {
      myFunc1(afterAll(callback,whenAllDone)); 
    }
    
    function myFunc1(value, callback)
    {
      console.log(value);
      callback();
    }
    
    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
    });
    
    最后但并非最不重要的一点是,如果你真的不想使用承诺,请使用fine库

    const arrayOfArg = ["one", "two", "three"];
    async.each(arrayOfArg, aCallback, err => {
        if(err) throw err; // handle the error!
        // carry on
    });
    

    有趣的图书馆;它是否总是假定函数接受两个参数,第二个是回调?感谢共享,但我认为第二个问题可能是个问题,因为所有函数都在递减同一个共享变量(在我的例子中是计数器)但不是它们自己的局部变量。@Xavier_Ex:只要您的计数器是在与
    for
    循环相同的函数中声明的,它就是函数中唯一的计数器(具有该名称),当您在回调中引用它时,您就是在引用该计数器,您应该会没事的。@icktoofay谢谢,但我不太明白你的解释。尽管计数器是函数中的局部变量,但它被传递给所有回调函数,因此它们实际上是在修改共享变量。你介意举个例子说明什么是问题吗?@Xavier_Ex:当然,它在所有回调中都是共享的,但不会与其他函数混淆。没有问题,;我之所以称之为“问题”,是因为您在问题中将其标记为“问题”。感谢您的分享,我可能需要先看看jquery“延迟”是如何工作的。我认为这与使用承诺是相同的解决方案。需要阅读承诺。谢谢分享,但这最终和柜台是一样的。我不知道公众的情况,但我确实觉得没有意义的计数器很烦人…@Xavier_Ex我不理解你的评论,任何任务的解决方案都会使用某种计数器。考虑<代码>毕竟<代码>作为库函数,在需要的地方使用它。是的,这就是我想知道的,如果使用计数器是唯一的方法。我知道一些并发问题可以通过协作例程来解决,因此我想在这里探讨一下可能性。最终,计数器不是我喜欢的解决方案