Я получал большие обещания за последний год. Я думаю, что два лучших ресурса, которые я извлек из сегодняшнего дня, — это выступление Forbes Lindesay на JSConf.EU 2013 и превосходная многообещающая статья Джейка Арчибальда на html5rocks .
Там были некоторые шаблоны, которые я использую снова и снова, поэтому я хотел поделиться и задокументировать их.
Обратите внимание, что используемые примеры в основном основаны на моем реальном коде, но были упрощены для демонстрационных целей.
1. Библиотека выбора
Во-первых, я предпочитаю использовать нативную реализацию и идти голыми руками. Я уверен, что это будет время, когда я захочу большего, чем может предложить нативный API, но я еще не пришел туда.
Как на стороне клиента, так и на стороне сервера, в стране узлов, поскольку обещания странным образом недоступны изначально, моя предпочтительная библиотека — это / обещание .
Я использовал RSVP в прошлом и слышал хорошие вещи о Bluebird .
RSVP кажется, что в основном это голые кости, но я узнал об prom.js ‘, denodeify
который преобразует основанную на обратном вызове функцию в функцию обещания, которая может быть очень полезной.
Update @ 2014-11-19 15:30:00 RSVP также имеет denodeify, и Мэтт Эндрюс из The FT выпустил отдельный модуль denodeify .
2. Чистые мелкие цепи
Это означает, что мой начальный код обещания будет выглядеть так:
writeFile(filename, content) .then(addDBUser) .then(dns) .then(configureHeroku) .then(function () { console.log('All done'); })
Это легко, если это все мои функции, но я также могу сделать это с помощью сторонних библиотек через denodeify
(особенность библиотеки обещать, хотя большинство библиотек обещаний имеют что-то похожее) — превратить функцию шаблона обратного вызова в функцию, основанную на обещаниях :
var writeFile = Promise.denodeify(fs.writeFile): writeFile(filename, content) .then(addDBUser)
Хотя я поймал одно место, denodeify
когда метод опирается на контекст метода, что, как выясняется, является большинством вещей ( fs
это просто случайность того, что его методы не зависят от контекста), поэтому убедитесь, что по bind
мере необходимости :
var addUser = Promise .denodeify(model.user.add) .bind(model.user) // multi line for blog readability
3. Предварительная выпечка
Вы уже видели, что я использую bind
, но я также обнаружил, что в некоторых ситуациях мне нужно вызывать функцию со статическими аргументами (то есть не полагаясь на предыдущее обещание), просто потому, что это часть цепочки обещаний.
Я мог бы сделать это:
writeFile(filename, content) .then(function () { return addUserToDb('rem', 'password', 'some-db'); })
Или то, что я обнаружил, что я более склонен делать сейчас, это предварительно addUserToDb
вызвать вызов статическими аргументами:
var addUser = addUserToDb.bind(null, 'rem', 'password', 'some-db'); writeFile(filename, content) .then(addUser)
Это также позволяет мне кодировать с мелкими цепочками, как указано выше, потому что (для меня) кажется немного многословным переходить в функцию, чтобы просто вернуть обещание, которое не зависит от какой-либо неизвестной переменной.
Следует обратить внимание на то, что если функция ведет себя по-разному, если есть больше аргументов, я должен холодно вызвать обещание.
Холодный вызов
Отказ от ответственности: этот шаблон требуется из-за моих собственных шаблонов предварительной выпечки и попыток (по иронии судьбы) упростить. Есть хороший шанс, что вам это не понадобится!
Когда функция работает как обещание и использует шаблон обратного вызова — это здорово, но я был пойман в прошлом.
То, как функция может работать под капотом, выглядит примерно так (этот псевдокод):
Heroku.prototype.post = function (slug, options, callback) { // do some async thing this.request(slug, options, function (err, data) { // ** this line is how the dual functionality works ** // if (callback) callback(err, data); // else do something with promise }); // return some promise created some place return this.promise; }
Так что post
можно назвать либо обещанием:
heroku.post(slug, opts).then(dostuff);
Или в качестве обратного вызова:
heroku.post(slug, opts, dostuff);
Но становится грязно, когда вы делаете это:
function configureHeroku(slug) { // prebake heroku app create promise var create = heroku.post.bind(heroku, '/apps', { name: 'example-' + slug } ); // prebake domain config var domain = heroku.post.bind(heroku, '/apps/example-' + slug + '/domains', { hostname: slug + '.example.com' } ); // ** this is where it goes wrong ** // return create().then(domain); }
Проблема в том, что когда domain
вызывается, он на самом деле вызывается с предварительно запеченными аргументами слага и опций, а также с разрешенным значением из create()
— так что получается третий аргумент .
Этот третий аргумент является разрешенным результатом, create()
который рассматривается как callback
аргумент и как объект функции, поэтому код будет пытаться вызвать его — вызывая исключение.
Мое решение состоит в том, чтобы заключить в холодный вызов — то есть вновь созданную функцию, которая вызывает мой метод без аргументов. Подобно bind, но затем никогда не допускайте никаких новых аргументов, также известных как curry (вот простая демонстрация типа вещи curry / частичный / seal ):
function coldcall(fn) { return function () { fn(); }; } function configureHeroku(slug) { // prebake heroku app create promise // ... // ** now it works ** // return create().then(coldcall(domain)); }
Примечание: вы можете сделать это используя curry , т.е. lodash.curry .
Теперь domain
вызов работает, потому что он вызывается, предотвращая добавление дополнительных аргументов.
4. Брось явный отказ
Вместо:
// compare password & input password return new Promise(function (resolve) { bcrypt.compare(input, password, function (error, result) { if (err || !result) { // reject and early exit return reject(err); } resolve(result); }); });
Я выброшу вместо отклонения
// compare password & input password return new Promise(function (resolve) { bcrypt.compare(input, password, function (error, result) { if (err) { throw err; } if (!result) { throw new Error('Passwords did not match.'); } resolve(result); }); });
Это может быть немного спорным. На самом деле, когда я выбросил это в твиттер, большинство людей вернулось с чем-то вроде:
Отклоняйте всякий раз, когда это возможно, это более производительно, потому что throw разбивает стек.
Это вполне может быть так, но в моем коде есть несколько ключевых преимуществ:
- Я привык к обработке первой ошибки, и довольно часто я случайно получаю
reject
в качестве первого аргумента, что приводит к большой путанице. Таким образом, я только когда-либо принимаю вresolve
качестве аргумента. - Я не должен помнить
return reject
. Я видел код, который не возвращается при отклонении, и затем он переходит кresolve
значению. Некоторые библиотеки выполняют, некоторые отклоняют, некоторые выдают новые ошибки. Бросив ошибку полностью избегает этого. - Это также согласуется с тем, как я буду исправлять ошибки в последующих
then
вызовах:
// compare password & input password utils.compare(input, password) .then(function () { if (!validUsername(username)) { throw new Error('Username is not valid'); } // continues... }) .then(etc)
Джейк также добавил пару полезных ответов:
Отклонение должно быть аналогичным броску, но асинхронным. Так что отвергайте то, что вы выбросили (что обычно является ошибкой)
Затем ссылка на его пост с «в ES7 асинхронные функции отклонить это бросить» . Это также подтверждает, что вы хотите отклонить с реальной ошибкой, а не строкой.
5. Всегда заканчивайте добычей
Я нередко проверяю http-запрос с обещанием, и он просто никогда не возвращается …
Проблема в том, что обещание где-то было отклонено и его не поймали. Поэтому я всегда заканчиваю добычей . Даже если это дамп на консоль, я знаю, что что-то не получилось.
writeFile(filename, content) .then(addDBUser) .then(dns) .then(configureHeroku) .then(function () { console.log('All done'); }) .catch(function (error) { console.error(error.stack); });
Этот последний улов позволяет мне увидеть полную трассировку стека относительно того, что пошло не так, и , что важно, что-то пошло не так .
Примечание: .catch()
только в спецификации ES6 и не появляется в Promises / A +, поэтому в некоторых реализациях библиотеки отсутствует .catch()
поддержка (как я обнаружил в mongoose, поскольку это зависит от библиотеки mPromise ).
резюмировать
Итак, это все:
- Неглубокие цепи
- Предварительная выпечка, где я могу, и холодный звонок, если необходимо
- Всегда бросай
- Всегда ловить
Довольно просто Мне было бы интересно услышать, какие шаблоны появляются в вашем рабочем процессе.