Перевод первой части отличной статьи про промисы. Базовые приемы создания и управления промисами.
Промисы используются для операций, вычисление которых занимает неопределенное время. Примером подобной операции может быть сетевой запрос, когда мы запрашиваем данные у API и не можем точно определить, когда будет получен ответ.
Если есть другие операции, выполнение которых зависит от этого сетевого запроса, то вырисовывается проблема. Без промисов нам придётся использовать вереницу колбэков(callbacks), чтобы выстроить последовательность операций. Это нормально, если у нас одно асинхронное действие. Но если нужно сделать несколько последовательных асинхронных шагов, колбэки становятся неуправляемыми и результат печально известен как лапша колбеков (callback hell)
doSomething(function(responseOne) {
doSomethingElse(responseOne, function(responseTwo, err) {
if (err) { handleError(err); }
doMoreStuff(responseTwo, function(responseThree, err) {
if (err) { handleAnotherError(err); }
doFinalThing(responseThree, function(err) {
if (err) { handleAnotherError(err); }
// Выполнено
}); // конец doFinalThing
}); // конец doMoreStuff
}); // конец doSomethingElse
}); // конец doSomething
Промисы предоставляют стандартизированный и понятный метод решения задач, которые должны выполняться последовательно.
doSomething()
.then(doSomethingElse)
.catch(handleError)
.then(doMoreStuff)
.then(doFinalThing)
.catch(handleAnotherError)
Создание промисов
Промисы создаются при помощи конструктора промисов. Он представляет собой функцию с двумя аргументами (resolve
& reject
) в качесте параметров.
var promise = new Promise(function(resolve, reject) { /* Содержимое промиса */ } )
Внутри этой функции мы можем выполнять любые асинхронные задачи. Чтобы отметить промис как исполненный, мы вызываем resolve()
, передавая ему значение, которое мы хотим возвратить. Что бы отметить промис как отклонённый или неудачный, мы вызываем reject()
, передавая ему сообщение ошибки. До того, как промис станет исполненным или отклоненным, он находится в состоянии ожидания.
Вот промис версия XMLHttpRequest -
/* CREDIT - Jake Archibald, http://www.html5rocks.com/en/tutorials/es6/promises/ */
function get(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
resolve(req.response); /* ПРОМИС ВЫПОЛНЕН */
} else {
reject(Error(req.statusText)); /* ПРОМИС ОТКЛОНЁН */
}
};
req.onerror = function() { reject(Error("Network Error")); };
req.send();
});
}
Использование промисов
Что бы выполнить промис, мы можем вызвать его как любую обычную функцию. Но, так как это промис, у нас есть доступ к .then
методу, который мы можем добавить к функции и который будет исполнен когда промис выйдет из режима ожидания.
.then()
метод принимает два необязательных параметра. Первый — это функция, которая вызывается, когда промис исполнен(resolved). Второй — функция, которая выполняется если промис отклонён(rejected).
get(url)
.then(function(response) {
/* successFunction */
}, function(err) {
/* errorFunction */
})
Обработка ошибок
Так как оба параметра (successFunction и errorFunction) опциональны, мы можем разделить их на два .then()
для лучшей читаемости.
get(url)
.then(function(response) {
/* successFunction */
}, undefined)
.then(undefined, function(err) {
/* errorFunction */
})
Что бы сделать код еще более понятным, мы можем использовать .catch()
метод, который является сокращенным вариантом для .then(undefined, errorFunction)
get(url)
.then(function(response) {
/* successFunction */
})
.catch(function(err) {
/* errorFunction */
})
Формирование цепи
Настоящая ценность промисов заключается в том, что мы можем выполнять несколько асинхронных функций по порядку. Мы можем объединить .then()
и .catch()
вместе для создания последовательности асинхронных функций.
Мы можем сделать это, возвращая еще один промис после выполнения или отклонения предыдущего. Например -
get(url)
.then(function(response) {
response = JSON.parse(response);
var secondURL = response.data.url
return get( secondURL ); /* Возвращаем новый промис */
})
.then(function(response) {
response = JSON.parse(response);
var thirdURL = response.data.url
return get( thirdURL ); /* Возвращаем новый промис */
})
.catch(function(err) {
handleError(err);
});
Если промис исполнен(resolved), то вызовется ближайший .then()
в последовательности. Если промис отклонён(rejected), то ближайший .catch()
в последовательности.
Паралелльное выполнение промисов
Может возникнуть ситуация, когда нам понадобится выполнить несколько промисов параллельно, и продолжать алгоритм только после того, как все промисы будут выполнены. Например, если мы хотим получить ряд изображений и только после этого отобразить их на странице.
Чтобы это сделать, нам необходимо использовать два метода. Это Array.map()
, для того, что бы применить промис для каждого элемента массива и сохранить результат в новый массив. И Promise.all()
, который выполнит resolve()
в случае исполнения всех промисов в массиве. Если хоть один промис в массиве будет отклонен, Promise.all()
тоже будет отклонён.
var arrayOfURLs = ['one.json', 'two.json', 'three.json', 'four.json'];
var arrayOfPromises = arrayOfURLs.map(get);
Promise.all(arrayOfPromises)
.then(function(arrayOfResults) {
/* Сделать что-нибудь, когда все промисы в массиве зарезолвятся */
})
.catch(function(err) {
/* Выполняется, если хоть один промис в массиве отклонён */
})
Если мы посмотрим в сетевую панель (Network panel) инструментов разработки (Development tools), мы увидим, что все запросы случаются параллельно.
Если вам нужна поддержка IE и/или Opera Mini, используйте полифил.
Спасибо за внимание!
Комментарии (50)
vlreshet
14.10.2016 12:57Объясните идиоту (мне) — промисы только делают синтаксис в синхронном стиле, функции же остаются асинхронными? То есть, если, допустим, в nodejs сервере, использовать промисы на все асинхронные операции — то весь потом не будет блокироваться при этом? Промисы — просто удобная обёртка?
ArtemNIkolaev
14.10.2016 13:13+1- Да, функции остаются асинхронными.
- Да, поток не будет блокироваться.
- И да и нет. В простейших ситуациях промисы действительно больше похожи на удобную обёртку. Однако уже
.then()
и.catch()
дают нам мощный инструмент для управления последовательностью выполнения. А кроме этого есть ещеPromise.all()
иPromise.race()
. Эти методы подробно рассматриваются во второй статье Promises 102(ссылка на оригинал, статья будет переведена в течение ближайшей недели).
vlreshet
14.10.2016 13:40Спасибо! Теперь осталось понять нужны ли мне они, ведь по сути — что в коллбек надо функцию передавать, что в then надо функцию передавать. Те же яйца только в профиль.
inook
14.10.2016 13:52Почитай про async/await функции. В 7 версии node.js стандартные библиотеки promisify будут и уже async/await работает с флагом harmony.
bromzh
14.10.2016 13:56Те же яйца только в профиль.
Нет. Функция, переданная в
then
илиcatch
может что-то вернуть или возбудить исключение, т.е. можно использовать return и throw как в обычных функциях. Если функция завершилась нормально, то её результат попадает в следующий then, если было исключение — то exception попадёт в следующий catch. С обычными колбеками так не получится.
ArtemNIkolaev
14.10.2016 13:57+1Тут я с вами не соглашусь. Допустим, вам предлагают на выбор один из двух проектов:
Пример кода из первого проектаdoSomething(function(responseOne) { doSomethingElse(responseOne, function(responseTwo, err) { if (err) { handleError(err); } doMoreStuff(responseTwo, function(responseThree, err) { if (err) { handleAnotherError(err); } doFinalThing(responseThree, function(err) { if (err) { handleAnotherError(err); } // Выполнено }); // конец doFinalThing }); // конец doMoreStuff }); // конец doSomethingElse }); // конец doSomething
san9s
15.10.2016 08:03+1Единственное принципиальное отличие первого от второго заключается в том, что в первом случае кто-то, для устрашения, написал анонимные функции, и выглядит оно страшно, а во втором, используя промисы — таки написал какие-то НЕ анонимные функции. А кажется, что разница на лицо.
Однако, на самом деле, разницы не будет заметно до тех пор, пока не начнет работать async/await, позволяющий тут же вернуть значение из функции, а не обрабатывать его в следующей. Лапшичка (callback hell) никуда не делась, она просто перекрасилась в другой цвет.ArtemNIkolaev
17.10.2016 11:59Вы всё-таки не поняли что я имел ввиду, позвольте уточнить. По другому первый пример можно представить вот так:
Другая версия первого примераfunction doFinalThingCallback(err) { if (err) { handleAnotherError(err); } // Выполнено } // конец doFinalThing function doMoreStuffCallback(responseThree, err) { if (err) { handleAnotherError(err); } doFinalThing(responseThree, doFinalThingCallback); } // конец doMoreStuff function doSomethingElseCallback(responseTwo, err) { if (err) { handleError(err); } doMoreStuff(responseTwo, doMoreStuffCallback); } // конец doSomethingElse function doSomethingCallback(responseOne) { doSomethingElse(responseOne, doSomethingElseCallback); } // конец doSomething doSomething(doSomethingCallback); // конец doSomething
Sayonji
15.10.2016 02:32+1Для меня самое ценное в промисах это то, что .then можно вызвать когда угодно: до или после завершения выполнения тела, и всё сработает как надо. За счет этого промисы позволяют легко использовать подготовку действий заранее. Например, при открытии какого-то экрана приложения, вы уже можете создать промис, который получит информацию для следующего экрана. А при открытии этого следующего вызвать ему .then(fillPage). И будет неважно, успел запрос выполниться или нет, логика выглядит так же. В этом примере промисы уже не просто выпрямляют цепочку колбеков.
Dawnreader
14.10.2016 13:14Да. Просто более читабельный вид. Потом был шаг к упрощению при помощи генераторов, а в следующем стандарте вероятно появится async\await.
equicolor
14.10.2016 13:14Все верно, ваш код будет выполняться асинхронно, так же, как при использовании кэлбэков. Промис скорее другой подход, нежели обертка: вы можете предоставить возможность передать кэлбэк, а можете вернуть промис из функции. Второе предпочтительнее, первый подход считается устаревшим. Ну а синхронный и промис стили все-таки не одно и тоже. Совсем «синхронно» это оператор await
jMas
14.10.2016 13:18Разъясните,
await
просто останавливает поток выполнения до получения результата? Другими словами, это способ сделать асинхронный код синхронным?ArtemNIkolaev
14.10.2016 13:37+1Позвольте процитировать другую статью на хабре — Async/Await в javascript. Взгляд со стороны:
Async/Await в javascript. Взгляд со стороныГоворя общедоступным языком async/await?—?это Promise.
Когда вы объявляете функцию как асинхронную, через волшебное слово async, вы говорите, что данная функция возвращает Promise. Каждая вещь которую вы ожидаете внутри этой функции, используя волшебное слово await, то же возвращает Promise.
Alternator
14.10.2016 15:36Это скорее способ, чтобы асинхронный(но последовательный) код выглядел почти как синхронный линейный код
Отличие только в том, что внутренние асинхронные вызовы нужно предварять await-ом
При этом базовая функция приостановит свое выполнение(но запомнит состояние локальных переменных и стек), и освободит поток для других функций.
Когда же внутренний вызов завершится, исходная функция продолжит свое выполнение с запомненной точки
И самое приятное, что любое исключение(хоть синхронное, хоть асинхронное) может быть одинаково поймано с помощью try..catch
DarthVictor
14.10.2016 13:15+1Да. Я Вам даже больше скажу, в известных мне реализациях JavaScript (NodeJS, любой современный браузер) не существует механизмов превращения асинхронного кода в синхронный, равно как не существует и обратного механизма. Существуют обертки разной степени сложности реализации (promises, async&await), но все они не меняют физического способа вызова функции и работы runtime'а.
Kongo
14.10.2016 14:14+2Promises, как шаблон проектирования, предназначен не только для избавления от стилистически некошерных «ёлок», появляющихся при оформлении асинхронных вызовов. Да, они действительно ужасны. Но с асинхронными вызовами появляется риск и посерьёзней, чем сложность восприятия вложенных блоков. И это инверсия контроля. Обещания, это инструмент, созданный для помощи в инвертировании этой инверсии.
Чем же страшна инверсия контроля? На практике асинхронные вызовы будут происходить к чужому коду, и когда вы передаёте ему функцию обратного вызова, у вас не только нет никакого знания, когда она будет вызвана, но и нет никаких гарантий, что она будет вызвана вообще, или, если будет вызвана, то будет вызвана один раз. В случае же вызова, опять же нет никаких гарантий с какими параметрами это произойдёт. Также не стоит забывать, что делая асинхронный вызов вы попадаете в блоки из которых вам не будут выбрасываться исключения и нужно предусмотреть их генерацию при обработке результата.
Вот обещания и предоставляют стандартный паттерн по решению этих задач.
Envek
14.10.2016 23:50+1Есть ещё один плюс у промисов: если у вас операция по какой-то причине выполнится дважды или трижды (даже не могу сходу придумать пример), то традиционный колбэк сработает тоже дважды или трижды, промис же резолвится и вызывает свой колбэк только один раз, уже разрезолвенный промис ничего не делает.
aeiuo
14.10.2016 14:38-1Как по мне, то промисы не сильно лучше чем тот же callback hell. Вот await — другое дело. Интересно как с await-ами организовать такое:
Может возникнуть ситуация, когда нам понадобится выполнить несколько промисов параллельно, и продолжать алгоритм только после того, как все промисы будут выполнены.
Какой-нибудь особый параллельный for?ArtemNIkolaev
14.10.2016 14:57Не согласен, промисы при любом раскладе сильно лучше колбеков. Дождитесь перевода второй статьи Promises 102, где подробно рассказано о том, как их готовить. Так же рекомендую вам ознакомиться со статьёй У нас проблемы с промисами. Автор рассматривает всевозможные проблемы, которые могут возникнуть и возникают при неправильном использовании промисов.
По поводу вашего вопроса, я уже ссылался выше на статью Async/Await в javascript. Взгляд со стороны
Конкретно на ваш вопрос отвечает следующая цитата:
А если мне нужно одновременно получить результат от нескольких вызовов?Так как мы уже разобрались, что мы имеем дело с
Promise
. Следовательно можно использовать метод.all()
объекта Promise для решения такого рода задач.
async function unicorn() { let [rainbow, food] = await Promise.all([getRainbow(), getFood()]); return {rainbow, food} }
aeiuo
14.10.2016 16:46-2Более организованные коллбеки, но всё еще коллбеки.
Вот это вот «Promise.» хорошо бы убрать отовсюду вообще и добавлять по мере надобности новые ключевые слова например fork/join(All)
var a = fork getA(); // выполнение продолжается без ожидания var b = fork getB(); join; // выполнение приостанавливается пока все fork в этом scope не завершатся
Ohar
14.10.2016 17:26aeiuo
14.10.2016 17:43Я видел. Не нравится мне Promise, костыль это, возникший чтобы покрыть недостающие фичи языка.
Ohar
14.10.2016 17:53Очень интересное мнение.
А как бы вы предложили реализовать работу с асинхронными операциями?aeiuo
14.10.2016 18:56Уже есть async/await, осталось перестать вязать его с коллбеками и промисами, подогнать API на замену всяким XMLHttpRequest ну и добавить то, чего не хватает для сложных требований типа параллельно выполнить и подождать выполнения всех:
примерыvar valueFromAnySource = race AsyncHttp.get(url1); valueFromAnySource = race AsyncHttp.get(url2); finish; console.log(valueFromAnySource); var part1 = fork AsyncHttp.request(...); var part2 = fork AsyncHttp.request(...); join; console.log(part1 + part2)
// если нужно соединить тексты из списка URL в изначальном порядке // решение без распараллеливания: var text = '' for (var url in urls) { text += await AsyncHttp.get(url) } console.log(text); // решение с распараллеливанием: var texts = [] var index = 0; for (var url in urls) { texts[index++] = fork AsyncHttp.get(url) } join; console.log(texts.join(''));
Еще пример:
// в обычном мире: var p1 = new Promise( function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://google.com'); xhr.send(); xhr.onload = function () { resolve(this.response); } xhr.onerror = function() { reject(this.statusText); } } ); p1.then( function(result) { console.log(result); }) .catch( function(err) { console.log('error:' + err); });
// вымышленный мир без промисов и коллбеков: try { var response = await AsyncHttp.request('GET', 'http://google.com'); console.log(response.statusCode); console.log(response.content); } catch (err) { console.log('error:' + err); }
SDSWanderer
15.10.2016 00:37Промисы даже без async\await сильно лучше предложенного в первом примере, так как они позволяют думать о данных, а не о контроле выполнения. Второй пример не вымышленный, именно так оно и будет выглядеть с async\await, только вместо придуманного
AsyncHttp.request
будет вполне реальныйfetch
. А пример из "реальной жизни" будет выглядеть гораздо лучше если не запихивать промис в переменную, а навешивать then и catch непосредственно, и применить arrow functions:
fetch('http://google.com') .then(result => console.log(result)) .catch(error => console.log('error:', error))
SDSWanderer
15.10.2016 01:00+1Пример который по середине, только по людски:
Promise.all(urls.map(url => fetch(url))) .then(texts => console.log(texts.join('')))
Вобщем, не пытайтесь придумать велосипед, учите лучше современный javascript, все уже за вас придумали.
tenbits
14.10.2016 15:41+1Жаль, что не продуманы 2 вещи — прогресс и отмена. Особенно отмена, или как минимум прерывание цепочки
.then(...).then(...).then(...)
, предлагают самому в каждомthen
проверять "флаги". И жаль, чтоalways
таки не попал в спецификацию.Holix
14.10.2016 21:13+2Да, жаль. Хотя бы отмена настолько нужна, что мне пришлось самому написать свою версию Promise с хорошей отменой (nano-promise). Суть в том, что стандартный промис лишь наблюдатель за асинхронной операцией. Промис не имеет "рычагов" управления для остановки операции. В nano-promise при создании "Обещания" можно вернуть конструктору объект с методом cancel:
function get(url) { return new Promise(function (resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function() { if (req.status == 200) resolve(req.response); /* ПРОМИС ВЫПОЛНЕН */ else reject(Error(req.statusText)); /* ПРОМИС ОТКЛОНЁН */ }; req.onerror = function() { reject(Error("Network Error")); }; req.send(); // Самое главное return { cancel: function () { req.abort(); }}; }); var ps = get('/index.html'); ps.cancel(); // отменяем
Сама отмена не что иное, как вызов
reject(Promise.CANCEL_REASON)
. Ко всему прочему, если есть цепочка из промисов важно корректно отменить всю цепочку с возможными ответвлениями. Т.е. отмена последнего промиса в цепочке порождает отмену до самого начала(при необходимости), а отмена первого спускает отмену (как reject) до самого конца.
rinatr
14.10.2016 17:00-2Блин, как раз имено в сию минуту именно с этими promise вожусь. Правда они реализованы не как-то по своему по ходу.
Тот же define из require.js + стандартный promise из библиотеки — и приложение ломается.
Почему так, еще предстоит выяснить, но чёто чувствую, что сделаю как всегда: аля setInterval с проверкой состояния.
Но может быть сейчас прочитаю эту умную статью и попробую сделать как по уму
darkdaskin
14.10.2016 18:00Кстати, промисы из разных библиотек прекрасно комбинируются друг с другом.
Promise.resolve(1).then(v => q.resolve(v + 1)).then(v => jQuery.Deferred().resolve(v + 1)).then(v => console.log('done', v))
выведет
done 3
, как и ожидалось.
adasoft
15.10.2016 01:48Столкнувшись с Promise так и не понял, почему не реализован метод .any() (условно назовём его так), который был по аналогии с .all() выполнил все переданные промисы, но не останавливался при первом reject, а просто сохранял его state.
Подобный метод может использовать например при загрузке информации с нескольких источников, один или несколько из которых могут быть недоступны по каким либо причинам. Но логика работы при этом не должна нарушаться, просто не добавляются все элементы. Сейчас я просто вызываю последовательно методы загрузки и обрабатываю полученные данные в каждом resolve вместо одного общего.
Связанный с вышеуказанным пунктом. Возможно организовать динамическую цепочку промисов. Т.е не явно указанную как в примерах Promise.then().then().then(), но итерацию по списку заданных промисов с последовательным выполнение в указанном порядке, с учётом того что результат resolve (а может и reject) передаётся по цепочке от одного промиса к другому. Можно назвать этот метод .each()
Я понимаю, что подобный функционал можно реализовать вне промисов, но почему то хочется чтобы он был внутри.
ArtemNIkolaev
15.10.2016 01:57+1Ответ на ваш вопрос можно найти в статье У нас проблемы с промисами или вот под спойлером:
Продвинутая ошибка №3 — промисы против фабрик промисовДопустим, вы хотите выполнить серию промисов один за другим, последовательно. Вы хотите что-то вроде
Promise.all()
, но такой, чтобы не выполнял промисы параллельно.
Сгоряча вы можете написать что-то подобное:
function executeSequentially(promises) { var result = Promise.resolve(); promises.forEach(function (promise) { result = result.then(promise); }); return result; }
К сожалению, пример выше не будет работать так, как задумывалось. Промисы из списка, переданного в
executeSequentially()
, все равно начнут выполняться параллельно.
Причина в том, что по спецификации промис начинает выполнять заложенную в него логику сразу после создания. Он не будет ждать. Таким образом, не сами промисы, а массив фабрик промисов — это то, что действительно нужно передать в
executeSequentially
:
function executeSequentially(promiseFactories) { var result = Promise.resolve(); promiseFactories.forEach(function (promiseFactory) { result = result.then(promiseFactory); }); return result; }
Я знаю, вы сейчас думаете: «Кто, черт возьми, этот Java программист, и почему он рассказывает нам о фабриках?». На самом деле фабрика — это простая функция, возвращающая промис:
function myPromiseFactory() { return somethingThatCreatesAPromise(); }
Почему этот пример будет работать? А потому, что наша фабрика не создаст промис до тех пор, пока до него не дойдет очередь. Она работает именно как
resolveHandler
дляthen()
.
Посмотрите внимательно на функцию
executeSequentially()
и мысленно замените ссылку наpromiseFactory
ее содержимым — сейчас над вашей головой должна радостно вспыхнуть лампочка :)adasoft
15.10.2016 07:35«фабрика» это отличный термин. Но «лампочка» не загорается, наверное из-за «магии»
kurtov
17.10.2016 06:57Т.к. я не Java программист, то делаю таким образом:
function executeSequentially(promises) { var result = Promise.resolve(); promises.forEach(function (promise) { // result = result.then(promise); result = result.then(() => promise); }); return result; }
Через reduce еще изящней
function executeSequentially(promises) { return promises.reduce(function(first, next) { return first.then(() => next); }, Promise.resolve()); }
vintage
17.10.2016 08:19И оба варианта работают не так, как нужно, прочитайте внимательнее.
kurtov
17.10.2016 08:54Да, вы правы. На самом деле я не использую next как промис. Вместо того чтобы формировать массив промисов, я итерирую массив объектов, по которым нужно сделать последовательные ассинхронные вызовы и в then() создаю анонимную функцию возвращающую промис. Т.е. это как раз фабрика о которой шла речь.
Виноват, что бездумно упростил свой вариант без проверки и выдал как верный.
netch80
17.10.2016 12:02Почему promise API несимметричен относительно деления на первичные функции (в new) и вторичные (во then/catch)?
В первичных надо получать аргументами resolve и reject, и вызывать их явно (проверил на promise 7.1.1 для nodejs — простой return со значением игнорируется, как будто промис незавершён).
Во вторичных надо возвращать значение или генерировать исключение, параметров resolve, reject нет.
Я бы понял логику, или что
1) у всех функций-коллбэков возврат значения работает как resolve с этим значением, а генерация исключения — как reject,
или что
2) все функции-коллбэки имеют три входных параметра — input, resolve, reject,
а лучше — и то, и другое (можно вернуть значение в конце, а можно — вернуть исключение и выйти; а ещё лучше — предусмотреть специальный тип исключения для resolve).
Также нет возможности написать .resolve(значение), и аналогично для reject — тоже было бы значительно удобнее (и извне, как уже обсуждают, и изнутри). (Тогда можно было бы вторым параметром передавать сам объект промиса, для вызовов его методов.)
Или я не вижу каких-то хитростей, которые решают это, и которые можно найти только с заметным опытом их использования?
n0wheremany
Недавно уже было. Но на мой взгляд эта статья более понятна, без всякой лишней воды.
Stan_1
Да, буквально неделю назад разбирался с промисами, и потерял два дня. :( Толковых материалов мало, в итоге доходил своим умом. Эта статья — очень толкова. Жалко, ее не было две недели назад.