Как стать автором
Обновить

Комментарии 47

В недостатки нативных ES6 promises я бы еще добавил отсутствие метода finally() и не очень интуитивно понятный способ создания зарезолвленых промисов (Promise.all([])). С другой стороны, выкидывать лишние библиотеки из проектов всегда приятно :)
Можете пояснить, про Promise.all — для создания заресолвленного Promise нужно просто вызвать Promise.resolve?
Да, согласен, с Promise.all я затупил
не знаю, как в стандарте, а в jQuery можно вроде $.then(true).
В стандарте можно Promise.resolve(true)
.finally() можно заменить конструкцией .then().catch().then() и не выбрасывать exception заново в catch(), тогда будет считаться, что блок отлова ошибок ошибку поймал, обработал и вернул выполнение в нормальное русло.

По стандарту каждый блок .then() или .catch() выдает наружу новый promose, соотв. если в .catch() не перепрокинуть ошибку

.catch(function(e){
  // do something
  throw e;
})


и вернуть некий результат или другой primise, то будет считаться, что нормальный ход выполнения восстановлен.
Эх, если уж так хотите es6 — берите co+генераторы, они куда лучше. Откуда эта истерия про то что промисы — панацея?

Да, промисы помогают решать мелкие полезные задачи, но общий workflow на них проектировать — себе дороже.
Я это понял в один момент, когда реализовывал на них авторизацию на отзываемых токенах со сроком действия.
Было как-то так

getTokenInfoFor(token).then(function(tokenInfo){
    getUserInfo(tokenInfo.email).then(function(userInfo){
       if (userInfo.tokenRevokeTime > tokenInfo.issueTime) 
           return Q.resolve(userInfo);
       else 
           return Q.reject(errors.tokenRevoked);
    }); 
});


Только кода еще больше было нагромождено. Промисы хороши там, где идет цепочечная обработка данных, не более. Если сложная логика — всегда будет куча замыканий даже с промисами.

на генераторах же будет как-то так
co(function*(){
    var tokenInfo = yield getTokenInfoFor(token);
    var userInfo = yield getUserInfo(tokenInfo.email);
    if (userInfo.tokenRevokeTime > tokenInfo.issueTime)
        throw e;
    return userInfo;
})(function(err, res){
    ...
});


(я, правда, не пользовался ни разу возвращаемым значением, сделал код так более понятным, как мне кажется).

И как вы понятную и человекочитаемую рекурсию на промисах сделаете?
У меня регулярно бывают задачи по выборке из базы данных с рекурсивными условиями (сейчас, например — взять первые n элементов, отсортированных по приоритету, с условием, что берутся все элементы с заданным приоритетом, и общая выборка не меньше определенного числа), их на промисах охренеешь делать.

В общем промисы — да, это удобно. Но перегрето. Полгода-год назад вообще какая-то истерия про них была, непонятная для меня.
Как программист C#, я испытываю боль от одного взгляда на промисы. В дотнете промисам и сопрограммам соответствуют таски (Task) и асинхронные методы (async/await). Однажды попробовав второе, возвращаться к первому нет никакого желания. Ну да, оба лучше, чем колбэки, но всё равно ведь ужас, если вызовы не в цепочку. Читать невозможно.
Для node.js есть реализация async/await: github.com/yortus/asyncawait. Однажды попробовав, промисы использоваться больше не захочется.
в traceur тоже есть вроде как, экспериментальная. но мне что-то Co+генераторы удобнее будет
Тащить async/await в js — это уже перебор, пусть этот ужас останется в .NET. В js есть множество своих методов и паттернов работы с callback'ами. По мне так async выполняет туже работу, что и asyncawait, только в более близком к js стилю.
Что такого ужасного в async/await?
Синтаксис 1 в 1 такой же, как и с генераторами (которых все уже ждут-не дождутся), только возможностей больше, да и доступы уже сейчас.

Пример подсчета файлов в обоих случаях.
// все нужные require

// co + генераторы
var countFiles = co(function* (dir) {
  var files = yield fs.readdirSync(dir);
  var paths = _.map(files, function (file) { return path.join(dir, file); });
  var stats = yield _.map(paths, function (path) { return fs.statAsync(path); });
  return _.filter(stats, function (stat) { return stat.isFile(); }).length;
});

// async/await
var countFiles = async.cps (function (dir) {
  var files = await (fs.readdirSync(dir));
  var paths = _.map(files, function (file) { return path.join(dir, file); });
  var stats = await (_.map(paths, function (path) { return fs.statAsync(path); }));
  return _.filter(stats, function (stat) { return stat.isFile(); }).length;
});


Как можно радоваться от промисов при виде этого стройного кода я не понимаю.
Причем даже в сложных ситуациях это все не разваливается а продолжает выглядеть понятно, как синхронный код. О промисах и callback'ах с async такого сказать нельзя.
Как можно радоваться от промисов при виде этого стройного кода я не понимаю.

С точки зрения будущего стандарта языка (с которым лучше ознакомиться), да и текущего, идеологически более верно что-то в таком стиле:

var files = await promiseFs.readdir(dir)
  , paths = files.map((file)=> path.join(dir, file))
  , stats = await Promise.all(paths.map((path)=> promiseFs.stat(path)))
  , countFiles = stats.filter((stat)=> stat.isFile()).length;
Либо я так долго работал в таком стиле, либо все мои проекты и коллеги с кем я работаю придерживаются того, что js — это callbacks, async, promises, service bus и т.д. Но когда я вижу рекомендации писать js код в sync виде — выглядит это жутко.
Каких возможностей больше? На первый взгляд, при наличие генераторов async-await — просто лишняя сущность. Фактически одно и то же, только ключевые слова разные.
Например, несколько интерфейсов у async функции: она может возвращать thunk, promise или принимать обычный callback в традиционном стиле; await тоже принимает все варианты. Это, конечно, просто возможности библиотеки и аналог можно реализовать и в co, но на данный момент получается, что asyncawait лучше будет работать с существующим кодом.

Есть еще небольшая фича, которую, как мне кажется, с помощью генераторов сделать не получится: внутри async-функции любые другие async-функции можно вызывать без await. Т.е. для своего кода можно достичь результата, где весь код выглядит синхронным и await даже не упоминается, а если сделать аналог require('bluebird').promisifyAll(require('fs')) для async функций (т.е. async.asyncifyAll()), то и вообще весь код станет визуально синхронным.

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

Второе — это вылитые волокна бы получились :-)
Там промисы внутри

Basic Example

var async = require('asyncawait/async');
var await = require('asyncawait/await');
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs')); // adds Async() versions that return promises
var path = require('path');
var _ = require('lodash');

/** Returns the number of files in the given directory. */
var countFiles = async (function (dir) {
var files = await (fs.readdirAsync(dir));
var paths = _.map(files, function (file) { return path.join(dir, file); });
var stats = await (_.map(paths, function (path) { return fs.statAsync(path); })); // parallel!
return _.filter(stats, function (stat) { return stat.isFile(); }).length;
});
Генераторы это замечательно, но не панацея. Генераторы — возможность асинхронного ожидания результата функции, когда промисы — универсальный интерфейс асинхронных функций и способ обработки ошибок в них. async / await, что грозятся попасть в ES7, базируется как раз на связке генераторов и промисов.

Promise поддерживается всеми современными браузерами, для старых достаточно простого полифила, когда генераторы требуют компиляции в лютый трэш, при взгляде на который седеешь.
ну, так может не стоит просто смотреть в результат?) главное, что работает
А дебажить потом как, если не смотреть в результат?
эмгх, sourcemaps же.
Я недавно делал проект на TypeScript, где sourcemap тоже есть, однако нормально дебажить все равно неудобно, т.к. что-то где-то все равно не так названо может быть, либо не на ту строку брейкпоинт встает. Короче говоря, помогает, конечно, но с оговорками. Большими такими оговорками.
Не только это главное.
Промисы хороши там, где идет цепочечная обработка данных

Правильно, поэтому надо любые вещи стараться сделать цепочкой.

getTokenInfoFor(token).then(function(tokenInfo){
    return getUserInfo(tokenInfo.email).then(function(userInfo){ // подменяем результат выполнения первого промиса вторым
       if (userInfo.tokenRevokeTime < tokenInfo.issueTime) throw errors.tokenRevoked; // выброс ошибки приведет к reject всех промисов ниже по цепи
       return userInfo; // отдаем это как результат второго промиса
    }); 
})
.then(function(userInfo){ ... })
.catch(function(rejection){ ... });


Вы, фактически, применили антипаттерн из статьи, потеряв ваш промис. Если добавятся еще какие-то действия, то их можно подключать в цепочку хоть до посинения и без дикой вложенности.
Не вижу сильной разницы между моим и вашим кодом, вы просто заменили reject на throw, а resolve на return. Catch относительно дорогая операция, так что лучше ее избегать.

В реальном коде было четыре(!) уровня вложенности, и от них реально было никуда не деться. Я писал пример по памяти, так что…
Приглядитесь, в моем коде ошибки промисов не потеряются, а в Вашем — они никак не будут обработаны. А так разница мала, да. Насчет уровней вложенности, готов поспорить, что их можно было бы вывернуть из вложенности в обычную цепочку, если правильно модифицировать результат Promise'ов.
эмм. ну я же не весь код показывал все-таки.
Вы серьезно считаете, что ошибка авторизации по токену никак бы не обрабатывалась?
Реализовать волокна на генераторах, безусловно, можно, но это использование инструмента не по назначению (костыль со множеством ограничений). Если есть возможность лучше использовать нативные волокна (node-fibers, например). С ними асинхронность не просачивается за пределы асинхронной функции:
function getUserInfoByToken( token ) {
    var tokenInfo = getTokenInfoFor(token);
    var userInfo = getUserInfoByMail(tokenInfo.email);
    if (userInfo.tokenRevokeTime > tokenInfo.issueTime)
        throw e;
    return userInfo;
}


Для себя я реализовал несколько хелперов:
$jin.async2sync — превращает асинхронную (в стиле nodejs) функцию в синхронную, которая останавливает текущее волокно до окончания своего исполнения
$jin.sync2async — наоборот, превращает синхронную в асинхронную (при необходимости заворачивает её в волокно)

Рекомендую почитать эту статью, где объясняется почему генераторы — это плохо, а волокна — хорошо: howtonode.org/generators-vs-fibers
Впрочем, генераторы, конечно, лучше чем обещания :-)
Можно попробовать так:

var tokenInfo = getTokenInfoFor(token),
    userInfo = tokenInfo.then(function(tokenInfo){
        return getUserInfo(tokenInfo.email)
    });
Q.all(tokenInfo, userInfo).spread(function(tokenInfo, userInfo){
    if (userInfo.tokenRevokeTime > tokenInfo.issueTime) 
        return Q.resolve(userInfo);
    else 
        return Q.reject(errors.tokenRevoked);
}); 


Получается только один уровень вложенности и, видно, что от чего зависит
Попробуйте разрулить нетривиальный поток выполнения на callbacks, желательно с обработкой ошибок, если вы не напишете свою имплементацию Promises, код, скорее всего, будет невозможно читать и понимать

Все верно, промисы — навороченные колбеки. Но, как выше писали, это не панацея, есть и другие подходы. Я о событиях.
События для другого. Promise одноразовый по определению, события — сущность многоразовая. Не стоит мешать две разные концепции, их следует использовать строго там, где это дает наибольший профит.
Какая принципиальная разница, сколько раз вызвано событие? Да и сама «одноразовость» ограничена лишь спецификацией, кем-то придуманной, потому что так хорошо ложилось в концепцию. Я же говорю о том, что это не единственно возможная концепция.

Для меня лично одноразовость это минус. Именно по этому на каждый «then» создается новый объект (+несколько областей видимости). Это так же порождает и недостатки, указанные в посте. EventEmitter никогда не проглотит ошибку. И ещё куча минусов есть.
Promise.all() не имеет отношения к параллелизму. Тут речь лишь о том, что порядок вычисления не важен (асинхронности в смысле). С параллелизмом в js вообще все сложно.

В jQuery реализация promise A (без плюса), от чего на практике одни минусы. Сталкивался с тем, что объекту Deferred можно сделать reject(), после чего resolve() и обещание внезапно становится выполненным.
Скажите, пожалуйста, правильно ли я понимаю, что вызвать разрешение обещания можно только внутри функции, переданной аргументом в конструктор? То есть нельзя так же, как в jQuery, создать deferred-объект и потом где-нибудь, в произвольной точке кода, вызвать его разрешение?
Никто конечно не мешает вытащить функции разрешения наружу. Но это как раз и является лично для меня раздражающим фактором в конструкции Q.defer(), т.к. в случае с Revealing Constructor Pattern (т.е. как реализован Promise), разрешение возможно только внутри, что в большинстве случаев является необходимым и достаточным. Затрудняюсь придумать сценарий, когда Promise создается в одном месте, а резолвится в другом.
Ну вот у нас часто применяется примерно такой подход:

let deferred1=$.deferred(), deferred2=$.deferred();
function callbackFn1(arr){
…//обработка ответа № 1 сервера
deferred1.resolve(arr);
}
function callbackFn2(arr){
…//обработка ответа № 2 сервера
deferred2.resolve(arr);
}
function callbackFn3(arr1, arr2){
…//обработка, которой нужно оба ответа сервера
}
$.deferred().when(deferred1, deferred2).then(callbackFn3);
//в какой-то момент инициируется запрос № 1
$.request(AJAX-запрос_1, callbackFn1);
//а в какой-то другой момент независимо инициируется запрос № 2
$.request(AJAX-запрос_2, callbackFn2);


И как это переписать на промисы? Как-то, наверное, можно, но не похоже, чтобы это было удобно.
У вас спагетти получилось :-)

Я бы сделал так:

// первый ленивый загрузчик данных
var first = $jin.atom.prop({
    pull: atom => {
        $.request(AJAX-запрос_1, arr => atom.push( arr ) )
        throw new $jin.atom.wait( 'Request 1' )
    }
})

// второй ленивый загрузчик данных
var second = $jin.atom.prop({
    pull: atom => {
        $.request(AJAX-запрос_2, arr => atom.push( arr ) )
        throw new $jin.atom.wait( 'Request 2' )
    }
})

// ленивый запускатель обеих реквестов параллельно и рендерер результата в документ
var result = $jin.atom.prop({
    pull: atom => {
        var data = $jin.atom.get([ first , second ])
        return data[0].concat( data[1] )
    },
    notify: ( atom , next ) => document.body.innerHTML = next.join( '|' )
})

// запускаем синхронизацию сервера с клиентом
result.pull()

// обновляем первую пачку данных через 10 секунд
setTimeout( () => first.update() , 10000 )
Когда ж у меня найдется время Ваши атомы потестировать…

У меня есть просьба — можно статью о том, как использовать атомы в приложении на React, а еще лучше — с учетом Flux. По-моему, отлично бы вписалось. Например чтоб можно было сравнить с github.com/facebook/flux/tree/master/examples/flux-chat.
Пока что могу разве что написать как их использовать с AngularJS :-) С реактом очень слабо знаком.
Обещаю по аналогии с Angular написать для React, я и с тем и с тем знаком :) баш на баш
По курам :-)
в функции timeout не хватает promise.catch(reject); или я не прав?
Он там совершенно ни к чему, ловить мы внутри функции ничего не планируем.
timeout(Promise.reject(), 1e4).catch(function(){
  console.log('Да ладно? Мало того, что зря ждём таймаут, так ещё и unhandled rejection %)');
});
UPD. Я понял, о чем речь была.

function timeout(promise, ms) {
    return new Promise(function (resolve, reject) {
        promise.then(resolve).catch(reject); // вот так
        setTimeout(function () {
            reject(new Error('Timeout’));
        }, ms);
    });
}
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории