Страницы

среда, 2 июля 2014 г.

Async vs. Callback Hell

Короткая зарисовка на тему управления потоком выполнения с помощью async. В большом приложении профит от применения этого модуля трудно переоценить. Я использую его уже не первый год и сегодня попробую показать пару приемов. На мой взгляд async - лучшее на текущий момент лекарство от Callback Hell.


Например мы получаем данные...
function getData(cb) {
  setTimeout(function() {
    console.log('Getting data...');
    return cb([{a: 3}, {a: 5}]);
  }, 1000);
}

... после чего с ними необходимо произвести определенные манипуляции...
function add(obj, cb) {
  setTimeout(function() {    
    obj.b = obj.a + obj.a;
    console.log('%d plus %d = %d', obj.a, obj.a, obj.b);
    cb();    
  }, 1000);
}

Код нашего приложения может выглядеть следующим образом:
getData(function(data) {
  data.forEach(function(obj) {
    add(obj, function() {
      console.log('obj:', obj);
    });
  });  
});

function getData(cb) {
  setTimeout(function() {
    console.log('Getting data...');
    return cb([{a: 3}, {a: 5}]);
  }, 1000);
}

function add(obj, cb) {
  setTimeout(function() {    
    obj.b = obj.a + obj.a;
    console.log('%d plus %d = %d', obj.a, obj.a, obj.b);
    cb();    
  }, 1000);
}

Результат работы приложения:

Но если мы решим вывести в консоль результат только после завершения обработки всех данных, да еще и обработать ошибки, которые могут возникнуть в процессе наших асинхронных манипуляций, то модуль async будет очень даже кстати:
var async = require('async');

getData(function(data) {
  async.each(data, function(obj, next) {
    add(obj, next);
  }, function(err) {
    if (err) console.log('err:', err);
    console.log('data:', data);
  });
});

function getData(cb) {
  setTimeout(function() {
    console.log('Getting data...');
    return cb([{a: 3}, {a: 5}]);
  }, 1000);
}

function add(obj, cb) {
  setTimeout(function() {    
    obj.b = obj.a + obj.a;
    console.log('%d plus %d = %d', obj.a, obj.a, obj.b);
    cb();    
  }, 1000);
}


Усложняем задачу - после получения данных выполняем две асинхронные манипуляции с каждым полученным объектом параллельно:
var async = require('async');

getData(function(data) {
  async.each(data, function(obj, next) {
    async.parallel([
      function(cb){ add(obj, cb); }, 
      function(cb){ multiply(obj, cb); }
    ], next);    
  }, function(err) {
    if (err) console.log('err:', err);
    console.log('data:', data);
  });
});

function getData(cb) {
  setTimeout(function() {
    console.log('Getting data...');
    return cb([{a: 3}, {a: 5}]);
  }, 1000);
}

function add(obj, cb) {
  setTimeout(function() {    
    obj.b = obj.a + obj.a;
    console.log('%d plus %d = %d', obj.a, obj.a, obj.b);
    cb();    
  }, 1000);
}

function multiply(obj, cb) {
  setTimeout(function() {    
    obj.c = obj.a * obj.a;
    console.log('%d times %d = %d', obj.a, obj.a, obj.c);
    cb();
  }, 1000);
}


Для полноты ощущений добавим еще одну манипуляцию, которая будет выполняться только после завершения первых двух:
var async = require('async');

getData(function(data) {
  async.each(data, function(obj, next) {
    async.parallel([
      function(cb){ add(obj, cb); }, 
      function(cb){ multiply(obj, cb); }
    ], next);    
  }, function(err) {
    if (err) console.log('err:', err);
    async.each(data, function(obj, next) {
      subtract(obj, next);
    }, function(err) {
      if (err) console.log('err:', err);
      console.log('data:', data);
    });    
  });
});

function getData(cb) {
  setTimeout(function() {
    console.log('Getting data...');
    return cb([{a: 3}, {a: 5}]);
  }, 1000);
}

function add(obj, cb) {
  setTimeout(function() {    
    obj.b = obj.a + obj.a;
    console.log('%d plus %d = %d', obj.a, obj.a, obj.b);
    cb();    
  }, 1000);
}

function multiply(obj, cb) {
  setTimeout(function() {    
    obj.c = obj.a * obj.a;
    console.log('%d times %d = %d', obj.a, obj.a, obj.c);
    cb();
  }, 1000);
}

function subtract(obj, cb) {
  setTimeout(function() {    
    obj.d = obj.c - obj.b;
    console.log('%d minus %d = %d', obj.c, obj.b, obj.d);
    cb();
  }, 1000);
}


Но какого коллайдера мы дважды проверяем наличие ошибок если есть возможность получить их в одном месте?
Для правды жизни выкинем ошибку в самой первой функции:
var async = require('async');

getData(function(data) {
  async.waterfall([
    function(callback){
      async.each(data, function(obj, next) {
        async.parallel([
          function(cb){ add(obj, cb); }, 
          function(cb){ multiply(obj, cb); }
        ], next);    
      }, callback);
    },
    function(callback){
      async.each(data, function(obj, next) {
        subtract(obj, next);
      }, callback);
    }
  ], function(err) {
    if (err) console.log('err:', err.message);
    console.log('data:', data);
  });  
});

function getData(cb) {
  setTimeout(function() {
    console.log('Getting data...');
    return cb([{a: 3}, {a: 5}]);
  }, 1000);
}

function add(obj, cb) {
  setTimeout(function() {  
    if (obj.a === 3) return cb(new Error('Achtung!'));
    obj.b = obj.a + obj.a;
    console.log('%d plus %d = %d', obj.a, obj.a, obj.b);
    cb();    
  }, 1000);
}

function multiply(obj, cb) {
  setTimeout(function() {    
    obj.c = obj.a * obj.a;
    console.log('%d times %d = %d', obj.a, obj.a, obj.c);
    cb();
  }, 1000);
}

function subtract(obj, cb) {
  setTimeout(function() {    
    obj.d = obj.c - obj.b;
    console.log('%d minus %d = %d', obj.c, obj.b, obj.d);
    cb();
  }, 1000);
}


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

На этом прощаюсь, и да пребудет с вами сила :)