Долой callback hell или как работают promises?

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

doSomething(function(result) {
  // success
}, function(err) {
  // error
});

Соответственно, если нужно выполнить несколько действий подряд последовательно, то получается некрасивая конструкция:

doSomething1(function() {
  doSomething2(function() {
    doSomething3(function() {
    });
  });
});

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

for (var i = 0; i < a.length; i++) {
  doSomething(a[i], function() { ... }); // все doSomething работают параллельно
}

Некоторые набираются храбрости использовать рекурсию: для первого элемента вызвать doSomething, в коллбэке рекурсивно вызвать doSomething для следующего элемента, и удалить его из массива. Так до тех пор пока массив не окажется пустым.
такие
Примерно так же работают и Promises. Они позволяют записывать асинхронные функции в удобном последовательном виде. Код выглядит примерно так:

promise.then(function() {
  doSomething1();
}).then(function() {
  doSomething2();
}).then(function() {
  doSomething3();
});

Как же это работает?

Очевидно что каждый вызов then() добавляет функцию-аргумент в очередь запланированных функций, однако пока что не выполняет их. А когда же их выполнять?

Оказывается (а может я и неправ) в JavaScript есть всего лишь один способ выполнить функцию отложенно — setTimeout с нулевым интервалом. А значит для реализации promises на понадобятся:

  • очередь отложенных функций
  • объект с методом then() для добавления новых функций в очередь
  • рекурсивная функция, которая выполняет первую отложенную функцию из очереди и ожидает ее завершения
  • setTimeout(fn, 0) для запуска рекурсивной функции

Как узнать когда именно завершаются функции? Проще всего делать это явно — когда функция готова завершиться — пусть вызовет специальную функцию next(). Звучит сложновато потому что часто встречается слово «функция»? Сейчас, смотрим на код:

function chain(callback) {
	var queue = [];

	function _next() {
		var cb = queue.shift();
		if (cb) {
			cb(_next);
		}
	}

	setTimeout(_next, 0);

	var then = function(cb) {
		queue.push(cb);
		return { then: then }
	}

	return then(callback);
}

Здесь все как описано выше — очередь, рекурсивная функция, setTimeout и объект с методом then().

Пример использования:

chain(function(next) {
	console.log('1');
	setTimeout(function() {
		console.log('2');
		next();
	}, 1000);
}).then(function(next) {
	console.log('3');
	setTimeout(function() {
		console.log('4');
		next();
	}, 1000);
}).then(function(next) {
	console.log('5');
	next();
});

В примере на экран выводится «1», потом через секунду — «2» и «3», еще через секунду — «4» и «5».

Код можно сжать практически до размера твита (137 байт) — https://gist.github.com/zserge/7021626
Может быть даже добавлю его в 140byt.es.

А дальше можно сделать так, чтобы next() принимала аргументы — например, результат работы функции и объект-ошибку. А можно посмотреть существующие аналоги Promises/Futures. Главное, что теперь понятно что у них там внутри.