Pull to refresh

Comments 121

Полезная статья. Действительно типовые ошибки. Попадись мне эта статья годом ранее, на многие грабли не пришлось бы наступать самостоятельно.

Продвинутая ошибка №5 — «проваливание» сквозь промисы
А вот этого я не знал. Не ожидал такого поведения. Даже странно, как это я ещё не напоролся на это через какой-нибудь баг. Правда не совсем понятно, зачем они так сделали?!

Ждем async/await
А пока ждём, на серверной стороне, можно использовать связку co + generator-ы или ей подобную. Получаем практически тоже самое, с практически таким же синтаксисом. Причём без транспиляторов.
co — медленная библиотека. Используйте bluebird.coroutine.
Опа… а catch есть? в описании не видно
Решил сравнить. Оказалось, что для bluebird.coroutine нужно предварительно все генераторы конвертировать в методы, возвращающие promise. Т.к. yield-ить итераторы генераторов она из коробки не умеет. Подключил такую возможность ― получил прирост производительности (в синтетическом тесте) примерно на 1/3. Нууу… неплохо :)
Также такой подход очень удобен для отлавливания любых синхронных ошибок. Он настолько удобен, что я использую его почти во всех методах API, возвращающих промисы:
<...>
Просто запомните, любой код, который может выдать синхронную ошибку — потенциальная проблема при отладке из-за «проглоченных» ошибок. Но если вы обернете его в Promise.resolve(), то можете быть уверены, что поймаете ее при помощи catch().

Ну-ну. Больше коллбэков для бога коллбэков!
Здесь можно посмотреть визуализацию работы promise, может быть полезна для лучшего их понимая.
Могу ошибиться, но в примере к ошибке №1 получившийся код хоть и выглядит красивее, но работать не будет. По крайней мере, в исходной задаче видно, что put делается для каждого объекта из массива результатов, а в отрефакторенной — один put на весь массив.
Ну, этот момент потом в примере номер 2 разобран
Частично, но это не делает первый пример рабочим. Не покидает ощущение, что код просто подогнали так, чтобы с первого взгляда выглядел красивым. Если применить технику из второго примера на все шаги из первого, то картина может получиться сильно хуже.
После того, как в node.js появилась нормальная поддержка генераторов без специальных ключей можно просто пользоваться bluebird Promise.coroutine и получать красивый асинхронный код с нормальными try… catch… и ветвлениями и отменой операций без боли.

Почувствуй разницу между:

doSomething().then(function () {
  return doSomethingElse();
});


и

Promise.coroutine(function*(){
    yield doSomething();
    return yield doSomethingElse();
})();
Эти генераторы не такие уж веселые на вид. Вы реально их используете в продакшене?
Что в них невесёлого, когда есть Promise.coroutine?
В этом плане я совершенно согласен с Домеником Деникола, что генераторы для «синхронности в асинхронном коде» — это больше хак и workaround, они изначально задумывались для других целей. И async/await — именно для управления асинхронным потоком исполнения в приложении, а генераторы останутся для того, для чего были задуманы изначально (итерация, удобные ко-рутины).

image
Что за подвох? Откуда в решениях задачи finalHandler, если в условии его не было?
На мой взгляд, приведенная задача не охватывает большинства ошибок, которые в статьи были описаны. Например, я задачу решил верно, но при этом узнал много нового о промисах из статьи.
Мне кажется, или для пазла 2 ошибка в ответе? (и соответственно для всего, где похожие ситуации):
doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                  finalHandler(undefined)
                  |------------------|

finalHandler вызовется после того, как doSomethingElse закончит работу. Возможно, автор имел в виду, что если в then вернётся promise, то в then для then будет результат этого промиса?
Если бы перед doSomethingElse стоял return, то да, finalHandler ждал бы выполнения промиса из doSomethingElse. В нашем же случае мы имеем дело со «сторонним эффектом», где doSomethingElse выполняется, возможно даже возвращает промис, вот только использовать в finalHandler мы его не сможем.
Да, я это понимаю. Просто «схема» может сбить с толку тех, кто плохо знаком с промисами. Она ведь не совсем так должна выглядеть. doSomethingElse выполнится в любом случае до finalHandler, мы не знаем что он возвращает, да нас это и не интересует:)
Здесь скорей имеется в виду, что начнут они свое выполнение одновременно (почти). При этом внутри функции doSomethingElse может быть какой-нибудь долгий ajax-запрос, результат которого придет только через несколько секунд (предположим, что интернет барахлит), а значит закончит свою работу она значительно позже.
Если бы перед вызовом doSomethingElse стоял return, то finalHandler честно бы ждал результат ajax-запроса.
Но почему почти? У нас всё в одном потоке тут, вызовы идут подряд. То что return promise будет ждать выполнения этого самого промиса это просто то, как обрабатывается результат then.
.then(f1).then(f2)

Функция f1, которую мы в then передаём выполниться раньше чем f2.
Ну вот нифига себе, я правильно пишу, а мне минусы ставят. Как же так :( Хоть бы причину писали
Начнут выполнение одновременно? В javascript? Вы серьезно?
Простите за грубость, но вы написали такую ерунду, которую нельзя вообще писать.
Здесь скорей имеется в виду, что начнут они свое выполнение одновременно (почти).
Движок javascript выполнеяется в однопоточном eventloop, так что никакие две нормальные функции вообще никогда не могут выполнятся одновременно.
При этом внутри функции doSomethingElse может быть какой-нибудь долгий ajax-запрос, результат которого придет только через несколько секунд (предположим, что интернет барахлит), а значит закончит свою работу она значительно позже.
Функция, которая инициировала ajax-запрос всегда выполняется раньше, чем будет завершен этот ajax-запрос (и будет вызван его обработчик).
Если бы перед вызовом doSomethingElse стоял return, то finalHandler честно бы ждал результат ajax-запроса.
Нет, потому что сама функция doSomethingElse может не возвращать promise этого ajax-запроса.
Под «одновременно (почти)» я имел в виду не «параллельно», а «сразу, без ожидания». Я говорил не о том, когда функции начнут и завершат свою работу буквально. Я имел в виду полное завершение их внутренних задач, в асинхронной логике это получение результата.
Простите, что смутил вас и спасибо за ваше уточнение!
На этих схемах показан не синхронный поток выполнения, а асинхронный: под doSomethingElse(undefined) понимается не только сам вызов функции, но и завершение того промиса, который эта функция вернула.

Вот, кстати, еще одна проблема с промисами, неназванная в статье. Непонимание того факта, что как только в программу вводятся элементы асинхронности, потоки управления начинают выглядеть по-разному в зависимости от того, с синхронной или асинхронной точек зрения мы их рассматриваем.
Такой ответ мне нравится. Но, лучше на схеме указывать, что имеется в виду:-) А то гадать какая тут у автора точка зрения, это не очень круто.
Я понимаю, что в современном мире можно подогнать любую тему под практически любую технологию, и статья хороша, но…

Что этот перевод делает в блоге компании Mail.ru?
Эммм, может я что-то не понимаю, откуда finalHandler из примера 2 знает, возвращает function() {doSomethingElse()} что-то или не возвращает? Это в примере все довольно просто, а на практике там может быть 10-этажный switch или if/else, и там в зависимости от условий может возвращаться значение, а может и не возвращаться… Ну т.е. then(finalHandler) должен дождаться выполнения предыдущего then для того, чтобы понять, возвращает или не возвращает что-то он.
В примере 2 finalHandler не знает, что возвращает функция doSomethingElse. Мало того, она даже не знает, что doSomethingElse существует, потому что результат ее работы улетает «в пустоту». В примере 2 finalHandler сразу получает на вход undefined (значение, возвращаемое любой функцией по умолчанию).

Возникает вопрос, как можно пробросить результат работы doSomethingElse в finalHandler? Ответ простой — добавить перед ее вызовом return:

doSomething().then(function () {
    doSomethingElse();
  })
  .then(finalHandler); // undefind

doSomething().then(function () {
    return doSomethingElse(); // "возвращаем" результат работы вложенной функции
  })
  .then(finalHandler); // тут finalHandler получит на вход результат doSomethingElse
  // и, если это промис, то дождется его "выполнения", а значит и результат его выполнения
Хорошо, что получит finalHandler в следующем случае?

doSomething().then(function () {
if (my_var == 2) {
return doSomethingElse();
} else {
doSomethingElse();
}
})
.then(finalHandler);
Либо результат работы doSomethingElse, либо undefined. Все зависит от переменной my_var.
А кто расскажет:
Вот вариант получше:
remotedb.allDocs(...)
  .then(function (resultOfAllDocs) {
    return localdb.put(...);
  })
  .then(function (resultOfPut) {
    return localdb.get(...);
  })
  .then(function (resultOfGet) {
    return localdb.put(...);
  })
  .catch(function (err) {
    console.log(err);
  });


как в этом замечательном примере:
1. Добавить _разную_ обработку ошибок для localdb.put() и localdb.get()?
2. Внутри третьего блока then получить доступ к resultOfAllDocs и resultOfPut?
function appendArgs() {
    var args1 = arguments;
    return function() {
        return Array.prototype.concat.call(args1, arguments);
    }
}

remotedb.allDocs(...)
  .then(function (resultOfAllDocs) {
    return localdb.put(...)
      .then(appendArgs(resultOfAllDocs), function(err) { /*Обработка ошибки 1*/ })
  })
  .then(function (resultOfAllDocs, resultOfPut) {
    return localdb.get(...)
      .then(appendArgs(resultOfAllDocs, resultOfPut), function(err) { /*Обработка ошибки 2*/ })
  })
  .then(function (resultOfAllDocs, resultOfPut, resultOfGet) {
    return localdb.put(...)
      .then(appendArgs(resultOfAllDocs, resultOfPut, resultOfGet), function(err) { /*Обработка ошибки 3*/ })
  })
  .then(function (resultOfAllDocs, resultOfPut, resultOfGet, resultOfPut2) {
    /* Какая-нибудь окончательная обработка результатов */
  })
  .catch(function (err) {
    console.log(err);
  });
Еще вы можете использовать промежуточные catch().

Promise.resolve('some text')
  .then(myText => {
    console.log(`My text is "${myText}"`);
    throw new Error('my error');
    return 'ololo'; // Эта строка не выполнится
  })
  .catch(errMessage => {
    console.log(`I caught an error "${errMessage}"`);
    return errMessage;
  })
  .then(someText => {
    console.log(`The message is "${someText}"`);
  });
В примере с продвинутой ошибкой №1 автор переусердствовал с Promise.resolve(). Непонятно, чем это
function somePromiseAPI() {
  return Promise.resolve().then(function () {
    doSomethingThatMayThrow();
    return 'foo';
  }).then(/* ... */);
}

Лучше обычного конструктора Promise:
function betterPromiseAPI() {
  return new Promise(function (resolve) {
    doSomethingThatMayThrow();
    resolve('foo');
  }).then(/* ... */);
}

Exception в функции, переданной в new Promise также будет перехвачен и вызовет reject. Зато во втором варианте создается на один промис меньше.
Проблема в том что не многие понимают что async/await это не панацея и стоит понимать где его использовать.

function save(){
    return new Promise((resolve, reject)=>{
         setTimeout(()=>{
              resolve(1);
         },300);
    })
}
async function test(){
     let one  = await save();
     // one = 1
     return one;
}


И вот вопрос что же вернет функция test() ?
Promise


И это еще более странно, вот этот вот код не даст тот результат который вы думаете если будите использовать babel с asyncToGenerator (nodejs 4.2+).

Кошмарный результат!
   let one = test();
   let two = 2;
   let three = one + two;
   // 3 ?? - НЕА

Точно хотите знать ?
[object Promise]2



В чем соль подобного, ссылка на стандарт (Листать до синтаксического сахара за счет чего это сделано)

А как решить такую ситуацию ?
Как не странно обернуть еще раз в async/await
async function test2(){
   let one = await test();
   // one = 1
   let two = 2;
   console.log(one + two); // 3
}
test2();


Любая асинхронная функция возвращает промис. Что в этом внезапного и неожиданного?
А как решить такую ситуацию ?
test().then(one => console.log(one + 2));

Ну или да, выполнять test через await в рамках другой async функции.
Я не совсем понимаю ― а на что вы рассчитывали? На то, что метод test заблочит весь поток js-а, до тех пор, пока не выполнится?
Я не совсем понимаю ― а на что вы рассчитывали? На то, что метод test заблочит весь поток js-а, до тех пор, пока не выполнится?

Конечно же нет, но представьте как это может быть воспринято человеком только начавшим уже с es7 (c babel), все показывают красивые примеры, не вдаваясь в подробности. И ссылка на сахар за счет чего это сделано дает понять что результат будет промис.
Ну… Любой асинхронный код это дискомфорт, в той или иной мере. К тому, что нельзя будет магическим образом писать асинхронный код синхронно я был уже готов после генераторов. Одним из неприятных моментов является то, что javascript удобно использовать в функциональном стиле, но «контекст» async-а (или generator-а) теряется в анонимных методах. Т.е. вот так вот сделать нельзя:

async function test(list)
{
  return list.map(el => await someAsyncMethod(el));
}

Потому что await не будет работать внутри метода. Тоже самое и в генераторах (yield). Такие библиотеки как underscore, lodash и пр. располагают к использованию цепочек методов с callback-ами. Да даже нативные методы .map, .reduce, .some и пр. тут ставят нам грабли.

Конечно, какие-то конструкции для удобства можно написать самому, к примеру, свою универсальную реализацию .map, которой и метод и async сгодится. Но, ИМХО, проще смириться, что за любую асинхронщину нужно платить удобством.

Очень не люблю такие вот конструкции:
for(let i = 0, n = list.length; i < n; ++i)
{
  var el = list[i];
}

Но «for-of» лишит меня ключа, а .each yield (async) контекста. Что ж поделать… В любом случае это гораздо удобнее, чем callback-ый ад.
А зачем писать свою реализацию .map() и прочих? Что мы хотим получить на выходе вот этой функции из примера?

async function test(list) {
  return list.map(el => await someAsyncMethod(el));
}

маленькое но — в вашем примере map будет синхронным и будет дожидаться выполнения someAsyncMethod на каждой итерации.
Вот и мне интересно, что мы ожидаем получить из подобного рода конструкции, даже если бы она работала. Даже если ее переписать, чтобы она не использовала arrow-функцию и использовала async, что будет работать технически, то мы в итоге все равно получаем массив промисов, как если бы вообще не использовали await в коде выше.

Я понимаю, что товарищ faiwer говорил про «контекст», и ведь вроде как в стрелочных функциях тот же «this», что и за ее пределами, так почему бы не передавать и «async» — но это другое. Как по мне — несмотря на необходимость явно отмечать функции как «async» — код получается с виду очень даже синхронным (но на практике надо понимать, конечно, во что он разворачивается).
Даже если ее переписать, чтобы она не использовала arrow-функцию и использовала async, что будет работать технически, то мы в итоге все равно получаем массив промисов, как если бы вообще не использовали await в коде выше.

Гхм. Почему «массив промисов» а не массив результатов работы промисов?

Я понимаю, что товарищ faiwer говорил про «контекст», и ведь вроде как в стрелочных функциях тот же «this», что и за ее пределами

Прошу прощения, что запутал вас. Я вовсе не имел ввиду ни this, ни scope. Слово «контекст» я использовал в более широком значении. Имею ввиду то, что доступные возможности в рамках «тела» обычного function-а, generator-а и async function-а разные. В первом случае мы не можем применять ни await, ни yield; во втором только yield; а в третьем только await; Каждая стрелочная или обычная анонимная функция такой «контекст» прекращает, руководствуясь уже возможностями обычных функций. А так как async-функции и generator-ы это, по большому счёту, вообще не функции, то применять их в качестве обычных callback-ов мы не можем.

Надеюсь теперь стало яснее, что я имею ввиду :)
Ну, тут как ни пиши, результат работы самой async функции будет промисом, а .map() — синхронный. В случае

async function test(list) {
  var result = list.map(async function (el) {
    return await someAsyncMethod(el);
  });
  console.log(result);
  return result;
}

в консоль выводятся объекты промисов, а в случае
async function test(list) {
  var result = await* list.map(el => someAsyncMethod(el));
  console.log(result);
  return result;
}

результаты работы промисов.
теперь понял вас. Логично, что затолкав в .map async-и мы получим промисы :) Но я имел ввиду что-то похожее на вот это:

return await list.asyncMap(someAsyncMethod);

Где asyncMap сам async-метод, который await-ми переберёт list someAsyncMethod-ом.

Но даже написав свой .asyncMap мы толком ничего не выиграем. Потому, что для того чтобы цепочка из вот таких вот async-монстро-функций заработала, придётся изрядно по… возиться. И всё равно получится что попало.
Ну, в точности, как синхронно, конечно, не будет. Но с «await*», который, как говорят ниже, убрали из спека и с Promise.all получаем вполне работающий синхронный .map, но оперировать все равно придется промисами — впрочем, это в любом случае. .asyncMap вполне несложно реализовать, использовав внутри .map и Promise.all — так что тут я особой монструозности то и не вижу. Единственное — всегда нужно держать в голове, что там происходит за кулисами.
так что тут я особой монструозности то и не вижу

return await list.asyncMap(fn1)
       .asyncFilter(fn2)
       .reduce(fn3)
       .some(fn4);

А так? :)
Чем-то жертвовать придется :)

Если filter тоже должен быть асинхронным — то сложнее, но если мы после .asyncMap работаем чисто с результатами промисов — то можно использовать синхронно все остальное.

var items = await list.asyncMap(fn1);
return items
       .filter(fn2)
       .reduce(fn3)
       .some(fn4);
Чем-то жертвовать придется :)
Ага. Собственно об этом я и говорю. Так то да, await, async, yield это куда лучше, чем calback-hell или promise-hell :)
Ну собственно именно такое поведение и ожидается. Но на самом деле будет syntax error. Потому что await работает только в контексте async-function, а стрелочные функции — async-ами не являются.
А зачем писать свою реализацию .map() и прочих? Что мы хотим получить на выходе вот этой функции из примера?

Ну функция в примере сделала бы следующее (на самом деле будет syntax error):

async function test(list)
{
  var result = [];
  for(let el of list)
  {
    result.push(await someAsyncMethod(el));
  }
  return result;
}

Что делает someAsyncMethod не важно. Важно что он возвращает что-либо полученное асинхронно. В примере выше код стал более громоздким и менее читаемым. Функциональный подход часто бывает более наглядным.

Но пока у нас 1 map, нам по большому счёту плевать. А что если у нас типовая lodash цепочка методов? К примеру из 8 последовательных chain-вызовов? Придётся забить на lodash цепочку и написать всё в обычном стиле, через for-ы, if-ы и пр… А это, зачастую, бывает очень неудобным, в особенности после того как подсел на функциональный подход.

Т.е. получается что async-и и generator-ы НЕ позволяют писать асинхронный код также удобно, как и синхронный. И дело не только в том, что при более менее сложной логике async-и придётся плодить везде (что местами создаст определённые неудобства, потребует обёрток и пр. шаманств). А в том, что ограничения куда шире.

Но в любом случае, через async-ки и generator-ы писать сложный асинхронный гораздо удобнее.
Как по мне — вполне нормально можно сделать и с async/await:

async function test(list) {
  return await* list.map(el => someAsyncMethod(el));
}

await* давно удалён из предложения и, например, в babel 6 будет выпилен. Замените на Promise.all.
А есть где-то информация почему так? Все-таки Promise.all — это дополнительный шум в коде. Как-то я упустил этот момент с удалением «await*».
Кстати, асинхронные стрелки тоже могут быть удалены (хотя, думаю, маловероятно)
Пока код состоит из одной строки — да.
Ещё небольшой пример. Наверняка многие уже взяли за привычку использовать .forEach или _.each, для перебора массивов. Это удобнее, чем for-of потому, что мы не теряем индексы. В случае async-метода приходится использовать

for(let i = 0, n = list.length; i < n; ++ i)
{
  let el = list[i];
  // ...
}


Правда тут есть 1 решение. Дождаться пока node (я не использую babel) сможет вот так:

for(let [el, i] from iter(list))

Где iter будет костылём, который позволит перебирать массив массив на манер Map-а.

Честно говоря, я не слежу за ES6-7. В Array случаем уже не добавили возможность перебора for-of-м без потери индексов?
for(let [key, val] of ['a', 'b', 'c'].entries())console.log(key, val);

давно часть ES6 и никакие индексы не теряются. Ну а то, что V8 пока не поддерживает деструкцию — его проблемы — никто не мешает использовать транспайлер.
.forEach или _.each прекрасно заменяются на map. А что делать с map — мы уже знаем.
function test(list)
{
  return Promise.all(list.map(el => someAsyncMethod(el)));
}


Ну или если нужен обязательно асинхронный код, то так:
async function test(list)
{
  return await Promise.all(list.map(el => someAsyncMethod(el)));
}
mayorov невероятно, оказывается существует Promise.all. Фантастика. Это всё меняет.

Прошу читать мои комментарии не по диагонали. Что вы будете делать в случае underscore, lodash или какой-бы то ни было ещё цепочке, основанной не на промисах? Если у вас каррирования, монады или ещё что-нибудь в таком духе?

Скорее всего забьёте и перепишите на не-функциональный подход. О чём я и говорю.
А какие еще функции имеют смысл в асинхронном варианте? Просмотрел список функций того же lodash — и не нашел куда бы я мог передать лямбду, возвращающую промис кроме как в map.

А что делать с map я уже писал.
UPD: ах да, filter, как я мог забыть. Для него я напишу свою реализацию. Один раз.
В идеале как то так:

return await list
      .filter(fn1)
      .map(fn2)
      //
      .invoke(fn3)
      .reduce(fn4)
      .groupBy(fn5)
      .map(fn6)
      .value();

Получится весьма не тривиально :) Хотя да, более, чем подъёмно.
А какие еще функции имеют смысл в асинхронном варианте?
Любые из тех, что принимают callback-и. А это половина библиотеки. Из самых часто используемых: .flatten (либо .flatMap), .reduce, .filter, .transform, .each. К примеру, в .filter может потребоваться некая проверка значения memcahed по ключу.

К тому же стоит учесть, что очень часто они применяются в цепочке, а не раздельно друг от друга.

А ещё вы ошибаетесь приравнивания .map, .each и Promise.all. Как минимум некоторые реализации .each-а умеют break-ать при return false.
UFO just landed and posted this here
97 употреблений «промисов» в статье, давно меня так не корёжило. Не имел дела с яваскриптом, там что, правда прижился такой термин?
Нет, я не сомневаюсь, что в яваскрипте есть нечто, по-английски называемое «promise», но в статье-«переводе» употреблять такую кальку как-то уж совсем неприлично. Это перевод с латиницы на кириллицу, а не с английского на русский.
«обещание» не очень хорошо звучит, отложенные объекы — лучше конечно но это чуть чуть другое.
Ну англоязычные пользователи используют же как-то «обещания», а не какие-нибудь obeschaniya, а у нас почему-то любят оперировать терминами, вместо того чтобы оперировать смыслами.
«обещание» не очень хорошо звучит, отложенные объекы — лучше конечно но это чуть чуть другое.
Многократно встречал в сети promise-ы именно как «обещания». Можно сказать — уже привык к такому термину. Слух не корёжит. ИМХО, термин вполне подходящий.
UFO just landed and posted this here
Отличный перевод. Автору большое спасибо. Несколько раз порывался прочесть данную статью в оригинале и откладывал на потом. Сейчас прочел на одном дыхании. Проблема, к сожалению, как мне кажется в том, что программисты, особенно начинающие, не читают спецификаций, а основываются на Stackoverflow-советах и примерах. Есть отличная серия докладов Domenic Denicola про промисы. Всем советую.
Читал ее в оригинал. Отлично что сделали перевод на русский. Статься шикарная.
И такой вот вопрос у меня вчера возник

Правильно ли я понимаю, что если один из промисов в цепочке сделал reject, то вся цепочка прерывается и никакого значения наружу из обработчика reject мы не вернем. У меня была задача в случае reject одного промиса, вернуть другой.
Для этих целей и существует catch. Он тоже может возвращать результат или даже промис.

(Только осторожнее с JQuery — в 1 и 2й версиях там catch работает по-другому)
Перевод отличный!

Тем, кто пользуется jQuery.Deferred, необходимо помнить, что он не следует пункту 2.2.4 спецификации Promises/A+: функции выполняются по возможности синхронно, в текущем стеке выполнения, и throw внутри функций, переданных в then, не приводят к reject, а прерывают текущий стек выполнения, то есть в зависимости от последовательности развития событий callback'и могут вызываться как асинхронно, так и синхронно, что очень опасно — асинхронные функции могут внезапно стать синхронными.

Здесь хорошо описана разница между синхронными и асинхронными callback'ами, и делается упор на то, что, предоставляя интерфейс с callback'ами, необходимо делать их либо всегда синхронными, либо всегда асинхронными, но не смешивать.

С другими реализациями, которые следуют всем требованиям Promises/A+, promise поменяет свое состояние только после завершения всех выполняющихся функций, то есть стек при смене состояния promise и вызове callback'ов всегда будет «пустой» (содержать только вызовы функций «платформы» и не содержать вызовов функций приложения).

Например,
function getSomethingPromised() {
    var def = jQuery.Deferred();

    def.then(function () {
        throw new Error();
    });

    def.resolve();
    //< Выкинет Error отсюда, прерывая выполнение функции getSomethingPromised
    // и всех вызвавших её функций -- функция getSomethingPromised предстала как синхронная.

    doSomething(); //< Не выполнится!

    return def.promise();
}


function getSomethingPromised() {
    var def = jQuery.Deferred();

    def.then(function () {
        throw new Error();
    });

    setTimeout(function () {
        def.resolve(); //< Выкинет Error отсюда, прерывая выполнение только функции, обрабатывающей таймер.
    }, 1000);

    doSomething(); //< Выполнится!

    return def.promise(); //< Вернет promise в вызывающую функцию  -- функция getSomethingPromised предстала как асинхронная.
}

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

Например, передавая функцию в Array forEach, sort или map, я точно знаю, что моя функция будет вызвана только синхронно в процессе выполнения этих функций, и не будет больше вызываться после их завершения.

Той же предсказуемости хотелось бы и от функций асинхронной природы (например, реализующих сетевые запросы). В jQuery гарантия асинхронного вызова callback-функций, переданных в функции асинхронной природы или подвешенных на promise, не предоставляется, что приводит к необходимости в вызывающем коде предполагать оба варианта вызова переданных функций. Это требует от разработчика большего опыта и понимания происходящего, следовательно, повышается вероятность допустить ошибку, не обработав синхронный вариант вызова callback'ов.

Особенно подвержен ошибкам код, который меняет состояние объектов, перемежая изменения навешиванием обработчиков promise'ов. В случае синхронного вызова callback'ов из promise'ов, в зависимости от состояния primose'ов эти callback'и будут вызваны либо асинхронно, либо сразу при навешивании, и код внутри callback'ов может при некотором стечении обстоятельств получить управление раньше, чем изменение состояния объекта завершится.
Очень полезная статья. Всегда, когда использовал Deferred, было ощущение, что это какой-то костыль.
Уже тысячу лет как изобретены корутины. Когда, конец, их внедрят в JS и перестанут изобретать всякие deffered, promises, async-await?
Как вы видите решение простейшей задачи — сделать 2 параллельных HTTP-запроса, дождаться их выполнения и вывести результат — на основе сопрограмм?

А если надо вывести диалоговое окно (на html, не системное) и дождаться нажатия кнопки «ОК» — как вы будете действовать?
А какие проблемы?, посмотрите на GoLang, там «все в корутинах».
Проблема в том, что сопрограммы сами по себе ничего не могут. Все равно нужны примитивы для асинхронности.
Все равно нужны примитивы для асинхронности.
Все io вызовы идут «асинхронно», вот пример параллельных http запросов.
Ммм… я не вижу принципиальной разницы между использованием WaitGroup и Promise.all…

const urls = [
    "http://www.golang.org/",
    "http://www.google.com/",
    "http://www.somestupidname.com/",
];

await Promise.all(urls.map((url) => http.get(url)));


Вообще горутины это чуть больше чем корутины… это корутины которые еще и по тредам распределяются автоматически в рантайме. Вот в python корутины есть уже давно и народ ноет что мол «не потоки».

Для синхронного кода корутины без распределения по тредам профита не имеют, для асинхронного и для управления флоу async/await и промисы дают как по мне минимальную разницу в контексте языка построенного на event loop.

Файберы это уже другое дело…
я не вижу принципиальной разницы между использованием
В данном случае разницы нет, разница видна в большом приложении, когда одно написано в синхронном стиле, а второе обвешано промисами и колбеками, например если в синхронном «дереве» вызова появится асинхронная ф-ия, то все дерево придется перепиливать на async/promise а так же все связанные части проекта, когда с корутинами этого ничего делать не нужно.

Для синхронного кода корутины без распределения по тредам профита не имеют
Не знаю что вы имеете ввиду, но в Python есть gevent и greenlet и результат будет как от файберов, т.е. код синхронный, но работает асинхронно.
например если в синхронном «дереве» вызова появится асинхронная ф-ия, то все дерево придется перепиливать на async/promise а так же все связанные части проекта, когда с корутинами этого ничего делать не нужно.
Правильно ли я вас понимаю, что синхронное «дерево» остаётся синхронным, но необходимая часть выполняется асинхронно? Если да, то как это может работать не блокируя текущий поток? В JS он 1, всё таки.
Да, правильно.
На пальцах например так: когда вы вызываете «блокирующий» метод http.Get, он запускается асинхронно и при этом поток переключается на другие задачи, когда другие задачи так же переключатся в «ожидание» и для вашего метода будет готов результат, то поток переключится обратно на ваш метод http.Get, вернет результат и ваш код пойдет работать дальше, для вас это будет как обычный синхронный код.
В разных языках, библиотеках разные реализации но идея одна, посмотрите как работают файберы.

Вот почему vintage говорит что async/await, yields, promise, callbacks… — это «прошлый век».
В разных языках, библиотеках разные реализации но идея одна, посмотрите как работают файберы.
Года 3 назад я пытался использовать node-fibers. Работало оно крайне препогано. Часто падало с сегфолтами, существенные проблемы с отловом ошибок, практически невозможно было это дело как то дебажить. Может быть сейчас стало лучше.
Очень просто, корутина — это легковесный поток. Когда корутине нужно заблокироваться — она не блокирует системный поток, а переключает его на другой стек вызовов, позволяя ему продолжать обрабатывать события. И как только придёт нужное событие — происходит переключение обратно на стек корутины. Таким образом у нас получается много легковесных потоков в рамках одного системного потока, которые не жалко блокировать.
Вы меня запутали :)

Таким образом у нас получается много легковесных потоков в рамках одного системного потока, которые не жалко блокировать.
1. А как тут дело обстоит с гонками, дедлоками и пр.? Ведь если это легковесный поток, то это именно поток…
2. Что-то мне подсказывает, что заморозить кадр в async-методе, и, когда нужно, разморозить его обратно ― должно быть гораздо дешевле, нежели запуск отдельного потока, пусть и легковесного. Так ли это?

Или под «легковесным потоком» вы подразумеваете не настоящий поток, а его имитацию, которая только приостанавливает выполнение одного стека, возобновляя другой, стоящий в очереди? Если да, то чем это отличается от await?
1. Проблемы синхронизации есть только в вытесняющей многозадачности, когда поток может быть остановлен в любое неожиданное для него месте, либо при одновременной работе нескольких потоков (на разных ядрах) с одним ресурсом. Корутины — это реализация кооперативной многозадачности, корутина блокируется только когда сама этого захочет.

2. Запуск легковесного потока требует лишь выделить память под стек и всё, переключение между стеками — тривиальная операция.

3. async создаёт «легковесный поток» для каждой функции.
3. async создаёт «легковесный поток» для каждой функции.
Судя по тому что await — это «наследник» yield. То выходит что цикл обработчик создается в первой async ф-ии, и все дочерние async вызовы пробрасывают в него управление. Т.е. «легковесный поток» создается для «дерева» функций.
Что выведет этот код?

var counter = 0;
var options = { delta: 1 }

if (isDebug) trace(options);
increment(options)
increment(options)
increment(options)
console.log(counter);

function increment(options) {
    counter = counter + options.delta;
}

function trace (object) {
    for (var key in object)
      (key => {
        var value = object[key];
        Object.defineProperty(key, {
          get: () => {
            sendEventToServer("get", object, key, value);
            return value;
          },
          set: newValue => {
            sendEventToServer("set", object, key, newValue);
            value = newValue;
          }, 
        });
      })(key)
}
UPD: извиняюсь, совсем забыл. Три вызова increment надо запустить параллельно, а потом дождаться окончания их выполнения. В текущем виде будет 3 и только 3, параллельный вариант интереснее.
под «легковесным потоком» вы подразумеваете не настоящий поток, а его имитацию, которая только приостанавливает выполнение одного стека, возобновляя другой, стоящий в очереди?
Да.

чем это отличается от await
в целом они работают похоже, но у await подхода есть проблемы вызова асинхронных функций из синхронных и наоборот, проблема выше, ну «захламление» кода, увеличение числа кейвордов. В общем случае у await нет преимуществ перед корутинами, корутины — это следующий шаг, после async/await.
Кажется, вы путаете сопрограммы с легковесными потоками. Отличаются они наличием планировщика потоков (заметьте, термина «планировщик корутин» не существует!)

Вот эта часть —
Когда корутине нужно заблокироваться — она не блокирует системный поток, а переключает его на другой стек вызовов, позволяя ему продолжать обрабатывать события.
— невозможна без планировщика потоков.
Простейший планировщик потоков — event-loop — есть всегда.

Есть-то он есть, но сопрограммы не используют планировщик потоков. Даже простейший. Иначе они уже не сопрограммы, а легковесные потоки.
Человек не использует колёса для передвижения. Даже одно. Иначе это уже не человек, а инвалид.
Кажется, вы путаете «корутины» и «горутины». Вторые на Javascript невозможны в принципе — это однопоточный язык, и очень много кода на эту самую однопоточность завязано.
Горутины — это корутины в Го, сотни корутин могут работать в одном потоке (для этого они и задумывались). А вот модуль для node.js — возможность использования корутин/файберов в javascript.
Да как бы генераторы включены в стандарт ES2015, который уже принят, а с полифилами и babel.js работает и на ES5. Есть реализации, если вы почитаете комментарии в ветке, и даже не одна.

async-await?

async/await вы в этот список всетаки зря вписали так как по сути своей это синтаксический сахар для корутин при работе с асинхронными функциями. И для 90% юзкейсов это более чем удобный вариант.
Это скорее синтаксический сахар для генераторов. Корутины от генераторов отличаются наличием стека.
Вообще, я потыкал Babel — в js async-await не так плох как в «синхронных» языках, т.к. в основе event-loop.
— Из async ф-ии можно вызвать обычную, и эта обычная ф-ия может сработать как async (т.е. создать задержку) если вернуть Promise
— Из обычной ф-ии можно вызвать async ф-ию и она запуститься асинхронно, но возможно есть/будет способ навесить callback
— Исключения, вроде, работают как ожидается, правда traceback теряется.

Ну и естественно ничего не заблочится, конечно это не корутины, но в полне себе не плохо.
Есть патч для V8 который реализует файберы, но его наверно никогда не примут.
Sign up to leave a comment.