По следам недавних топиков, а также постоянных рассказов в стиле «мой стартап не взлетел, потому что его зохавала лапша из callback-ов».
Как раз недавно я закончил небольшой проект (ссылку не даю, чтобы не заподозрили — кому надо см. профиль), полностью и на всех этапах написанном только на JS, и притом полностью асинхронный. Разумеется, я столкнулся с пресловутой проблемой «лапши». И, вы не поверите, совершенно спокойно решил её без всяких там фреймворков и хитрых приемов.
Итак, допустим, у нас есть задача: асинхронно выбрать из базы количество книг, потом асинхронно же выбрать из базы нужную пачку книг, потом асинхронно же выбрать из базы метаданные по книгам, а потом свести всё это в один dataset и отрендерить шаблон. Как это обычно выглядит?
Если накинуть ещё пару-тройку промежуточных шагов, то всё станет совсем плохо.
Теперь зададим себе простой вопрос: зачем мы написали эту лапшу? Действительно ли нам необходимы здесь три вложенных замыкания?
Нет, конечно. У нас нет ровно никакой нужды из третьей анонимной функц��и иметь доступ к замыканиям второй и первой. Перепишем немного код:
Пожалуйста. Код стал полностью линейным, и, что немаловажно, более структурированным; весь program flow у вас перед глазами, логические блоки кода оформлены отдельными функциями. Никаких фреймворков и хитрых синтаксисов. И никаких подводных камней — dataset замкнут в контексте обработки пары request-response, случайно залезть в какие-то разделяемые между request-ами данные не получится.
Всё немного усложняется, если нужно что-то запараллелить. У меня такой задачи не было, но если бы была (допустим, есть два набора метаданных), то я решал бы её так:
Смысл всё тот же — создать отдельное замыкание для набора функций, который обычно объединяют в «лапшу», и расписать их последовательно.
Кажется, что в таком формате асинхронные callback-и не только не захламляют код, но и, напротив, делают его более структурированным и читабельным.
Как раз недавно я закончил небольшой проект (ссылку не даю, чтобы не заподозрили — кому надо см. профиль), полностью и на всех этапах написанном только на JS, и притом полностью асинхронный. Разумеется, я столкнулся с пресловутой проблемой «лапши». И, вы не поверите, совершенно спокойно решил её без всяких там фреймворков и хитрых приемов.
Итак, допустим, у нас есть задача: асинхронно выбрать из базы количество книг, потом асинхронно же выбрать из базы нужную пачку книг, потом асинхронно же выбрать из базы метаданные по книгам, а потом свести всё это в один dataset и отрендерить шаблон. Как это обычно выглядит?
exports.processRequest = function (request, response) { db.query('SELECT COUNT(id) FROM books', function (res1) { // do something db.query('SELECT * FROM books LIMIT ' + Number(limit) + ' OFFSET' + Number(offset), function (res2) { // do something 2 db.query('SELECT * FROM bookData WHERE bookId IN (' + ids.join(', ') + ')', function (res3) { // И вот наконец формируем как-то dataset response.write(render(dataset)); }); }); }); }
Если накинуть ещё пару-тройку промежуточных шагов, то всё станет совсем плохо.
Теперь зададим себе простой вопрос: зачем мы написали эту лапшу? Действительно ли нам необходимы здесь три вложенных замыкания?
Нет, конечно. У нас нет ровно никакой нужды из третьей анонимной функц��и иметь доступ к замыканиям второй и первой. Перепишем немного код:
exports.processRequest = function (request, response) { var dataset = {}; getBookCount(); function getBookCount () { db.query('SELECT COUNT(id) FROM books', onBookCountReady); } function onBookCountReady (res) { // ...заполняем dataset getBooks(); } function getBooks () { db.query('SELECT * FROM books LIMIT ' + dataset.limit + ' OFFSET' + dataset.offset, onBooksReady); } function onBooksReady (res) { // ... заполняем dataset getMetaData(); } function getMetaData () { db.query('SELECT * FROM bookData WHERE bookId IN (' + dataset.ids.join(', ') + ')', onMetaDataReady); } function onMetaDataReady (res) { // ... заполняем dataset finish(); } function finish () { response.write(render(dataset)); } }
Пожалуйста. Код стал полностью линейным, и, что немаловажно, более структурированным; весь program flow у вас перед глазами, логические блоки кода оформлены отдельными функциями. Никаких фреймворков и хитрых синтаксисов. И никаких подводных камней — dataset замкнут в контексте обработки пары request-response, случайно залезть в какие-то разделяемые между request-ами данные не получится.
Всё немного усложняется, если нужно что-то запараллелить. У меня такой задачи не было, но если бы была (допустим, есть два набора метаданных), то я решал бы её так:
function getMetaData () { var parallelExecutor = new ParallelExecutor({ meta1: getMetaData1, meta2: getMetaData2 }); function getMetaData1 () { db.query('smthng', onMetaData1Ready); } function getMetaData2 () { db.query('smthng', onMetaData2Ready); } function onMetaData1Ready (res) { // заполняем dataset parallelExecutor.ready('meta1'); } function onMetaData2Ready (res) { // заполняем dataset parallelExecutor.ready('meta2'); } parallelExecutor.start(onMetaDataReady); } function onMetaDataReady () { }
Смысл всё тот же — создать отдельное замыкание для набора функций, который обычно объединяют в «лапшу», и расписать их последовательно.
Кажется, что в таком формате асинхронные callback-и не только не захламляют код, но и, напротив, делают его более структурированным и читабельным.
