Как синхронизировать обратные вызовы JavaScript?

Я уже довольно давно занимаюсь разработкой на 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

Предлагаемое решение:

Initiate a counter at the beginning of the loop holding the length of the loop, и each callback decrements that counter. When the counter hits 0, execute myFunc2. This is essentially to let the callbacks know if it the last callback in sequence и if it is, call myFunc2 when it done.

Инициируйте счетчик в начале цикла, содержащий длину цикла, и каждый обратный вызов уменьшает этот счетчик. Когда счетчик достигнет 0, выполните myFunc2. По сути, это должно дать возможность обратным вызовам знать, является ли это последним обратным вызовом в последовательности, и если это так, вызовите myFunc2, когда это будет сделано. Проблемы:

  1. Счетчик необходим для каждой такой последовательности в вашем коде, и использование бессмысленных счетчиков везде не является хорошей практикой.
  2. Если вы помните, как поток конфликтует в классической проблеме синхронизации, когда все потоки все вызывают var-- для одного и того же var, могут произойти нежелательные результаты. Происходит ли то же самое в JavaScript?

Окончательный вопрос:

Есть ли лучшее решение?

Ответ 1

Хорошей новостью является то, что 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);
}

Первый переданный аргумент - это функция, которая выполнит следующую задачу, которая должна быть передана вашей асинхронной функции. Второй аргумент - текущая запись вашего массива значений.

Предположим, что задача asynch выглядит следующим образом:

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

Демо

Ответ 2

Вторая проблема не является проблемой, если каждый из них находится в отдельной функции, и переменная объявляется правильно (с var); локальные переменные в функциях не мешают друг другу.

Первая проблема - это скорее проблема. Другие люди тоже разозлились, и в итоге они превратили библиотеки в такой шаблон для вас. Мне нравится async. С его помощью ваш код может выглядеть так:

async.each(someArray, myFunc1, myFunc2);

Он также предлагает множество других асинхронных блоков. Я бы рекомендовал взглянуть на него, если вы делаете много асинхронного материала.

Ответ 3

Вы можете достичь этого, используя отложенный объект jQuery.

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

Ответ 4

С помощью этой вспомогательной функции:

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 создает прокси-функцию для обратного вызова, а также уменьшает счетчик. И вызывает, когда функция AllDone работает, когда все обратные вызовы завершены.

Ответ 5

один поток не всегда гарантирован. не ошибетесь.

Случай 1: Например, если мы имеем 2 функции следующим образом.

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

то перед возвратом функции1 будет вызываться функция2, если 1 поток гарантирован, то функция2 не будет вызвана до возвращения функции1. Вы можете попробовать это. и вы узнаете, что подсчет растет, пока вас предупреждают.

Случай 2: с Firefox, хром-код, перед возвращением 1 функции (без предупреждения внутри), можно вызвать и другую функцию.

Таким образом, действительно требуется блокировка мьютекса.

Ответ 6

Есть много, много способов добиться этого, я надеюсь, что эти предложения помогут!

Во-первых, я бы превратил обратный вызов в обещание! Вот один из способов сделать это:

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

Далее, используйте сокращение для обработки элементов массива один за другим!

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

Если вы хотите обрабатывать все элементы массива одновременно, используйте карту Promise.all!

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

Если вы можете использовать async/wait, вы можете просто сделать это:

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

// carry on

Вы даже можете использовать мою очень классную synchronize-async библиотеку, например:

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

И последнее, но не менее важное: используйте async, если вы действительно не хотите использовать promises.

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