19 декабря 2011

Последовательный вызов асинхронных функций

JavaScript
Как известно, язык JavaScript преследует парадигму событийно-ориентированного программирования. Это, безусловно, хорошо, но что делать, если за одной асинхронной функцией должна вызываться другая асинхронная функция, а затем еще одна, и еще… Иногда такой код очень запутывает, и не только человека привыкшего к синхронному и поочередному вызову функций. Это касается сложных анимаций, таймаутов, аякса, когда за одним должно следовать другое, и так дальше.

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

UPD
image
Ниже моё решение, являющееся аналогом этой функции модуля async и кучи других подобных решений, представленных в комментариях. Спасибо всем комментирующим и sedictor в частности.
/UPD

Рассмотрим пример (который взят из головы и в нем возможны ошибки) гипотетического парсера сайта, который после парсинга заносит данные в БД, и, после занесения, вызывает некоторый код.

var html = '';
request.on('response', function (response) {

    response.on('data', function (chunk) {
        html = html + chunk;
    });

    response.on('end', function() {
        //какой-то парсер
        parse(html, function(data){  
                //какая-нибудь функция, добавляющая данные в базу
		addToDatabase(data, function() {  
			doSomething();
		})
	});

    });
});


Много вложенных колбеков — не есть гуд, пробуем по-другому.

var html = '';
var responceOnEnd = function() {
    parse(html, parsed);
}
	
var parsed = function(data){ 
	addToDatabase(data, addedToDatabase)
}

var addedToDatabase = function() {
	doSomething();
}

request.on('response', function (response) {

    response.on('data', function (chunk) {
        html = html + chunk;
    });

    response.on('end', responceOnEnd);
});


Но здесь несколько лишних переменных, в которых так же можно запутаться.

Я предлагаю сделать вот так:

wait(function(runNext){
	request.on('response', runNext);
}).wait(function(runNext, response){
	response.on('data', function (chunk) {
		html = html + chunk;
	});
	
	response.on('end', function() {
		runNext(html);
	});
}).wait(function(runNext, html){
	parse(html, runNext);
}).wait(function(runNext, data){
	addToDatabase(data, runNext);
}).wait(function(){
	doSomething();
})


Интересно? Поехали дальше.

Функция wait.


//first — первая функция,которую нужно запустить
wait = function(first){ 
       	//класс для реализации вызова методов по цепочке
	return new (function(){ 
		var self = this;
		var callback = function(){
			var args;
			if(self.deferred.length) {
				/* превращаем массив аргументов
 				  в обычный массив */
				args = [].slice.call(arguments); 

				/* делаем первым аргументом функции-обертки
 				  коллбек вызова следующей функции */
				args.unshift(callback); 

				//вызываем первую функцию в стеке функций
				self.deferred[0].apply(self, args); 

				//удаляем запущенную функцию из стека
				self.deferred.shift(); 
			}
		}
		this.deferred = []; //инициализируем стек вызываемых функций

		this.wait = function(run){
			//добавляем в стек запуска новую функцию
			this.deferred.push(run); 

			//возвращаем this для вызова методов по цепочке
			return self; 
		}

		first(callback); //запуск первой функции
	});
}	


Не уверен, что код и комментарии прозрачны, самому несколько секунд приходится вдумыватья :)

Для наглядности работы я сделал несколько последовательных анимаций:

     wait(function(runNext){
        log('Первая анимация пошла');
		
        $('#div1').animate({
			top: 30
        }, 1000, function(){
            //передаем какие-нибудь аргументы в следующий вызов
            runNext(1,2); 
        });
        
    }).wait(function(runNext, a, b){
        //используем аргументы из предыдущего вызова
        log('Вторая анимация пошла, a='+a+' b='+b ); 
		
        $('#div2').animate({
			top: 50
        }, 1000, runNext);
        
    }).wait(function(runNext){
		log('Ждем две секунды');
		
        setTimeout(function(){
            log('Две секунды прошли')
            runNext();
        }, 2000);
        
    }).wait(function(runNext){
        log('Третья анимация пошла');
		
        $('#div3').animate({
			left: 50
        }, 1000, runNext);
        
    }).wait(function(runNext){
        log('Последняя анимация');
		
        $('#div1').animate({
			top: 0,
			left: 45
        }, 1000, runNext);
		
    }).wait(function(){
        log('Закончили');
    });
Запустить пример на JSFidle

Как это работает?
Первым делом вызывается функция wait, аргументом которой служит другая функция, запускаемая сразу же с одним аргументом, служащим коллбеком (в примере он определен как runNext) для вызова следующей порции кода. После выполнения коллбека, в который можно передать некоторые аргументы, полученные на текущем шаге, вызывается следующая функция, переданная в метод wait, причем первым аргументом этой функции является коллбек, вызывающий следующую часть скрипта, остальные — аргументы, переданные в коллбек на предыдущем шаге. И так далее.

Собственно, всё.
P. S. Как писалось выше, я не уверен в уникальности такой разработки, поэтому, если Хабрасообщество оценит эту краткую статью как банальность, она тихонечко отправится в черновики.
Судя по количеству добавивших статью в избранное, пост имеет некоторую ценность, поэтому, пожалуй, я не буду его прятать.
Теги:асинхронностьcallbackколлбекколбеккалбекпоследовательный вызовасинхронные функциисобытия
Хабы: JavaScript
+30
28,8k 179
Комментарии 33
Лучшие публикации за сутки