Pull to refresh

Comments 34

По-моему, главная особенность async/away то, что нельзя использовать away в функции, не объявив её async, ну и нельзя использовать вне функций. Как следствие, рано или поздно придётся использовать then в случаях, когда нас интересует возвращаемое значение в синхронном контексте.
В синхронном контексте первая в цепочке функция должна быть async, тогда не нужно использовать then. Если это не так, значит скорей всего что-то неправильно в архитектуре приложения.
Ну вот есть функция получения количества книг у автора типа async getBookCount(aythorId) {}, нужно его вывести в консоль (для простоты). Каким будет код основного скрипта (то есть в глобальном контексте, вариант — в обработчике onload) страницы? console.log(getBookCount(1)) не сработает ожидаемо, console.log(await getBookCount(1)) вообще не сработает. только что-то вроде getBookCount(1).then(console.log)

Ничего не мешает записать первый вызов из "глобальной" области вот так:


(async () => {
    await asyncFunction();
})();
return забыли. Что результатом этого вызова будет?
А куда он возвращать то будет, если это вызов в глобальной области? Это лишь пример как запустить функцию.

Это бессмысленно. Зачем выводить в консоль Promise?


Лучше вот так делать:


(async () => {
    console.log(await asyncFunction());
})();

Хотя лично мне все-таки проще написать asyncFunction().then(console.log);

Так я и хотел показать, что без then не получится вывести результат резолвинга промиса (return забыл перед await добавить). Можно всё выше и выше поднимать async, но рано или поздно придётся сделать then, если нас результат интересует.
Да, это «colored function» problem.
Проблема эта решается введением новой идиомы, волокон (fibers), которые по ряду причин не хотят пока добавлять в браузеры. Аналогии из других языков: go/goroutines, java/loom.
Есть предложение в стандарт top level await который уже (или пока) в stage-2. Если я всё правильно понял, то это как раз про ваш случай
Это частный случай, сахар над import. Типа, давайте в коде вместо:
(async () => {
  const [a, b, c] = await Promise.all([
    import('./a.mjs'),
    import('./b.mjs'),
    import('./c.mjs')
  ]);
  console.log(a, b, c);
})();
будем писать
import a from './a.mjs'
import b from './b.mjs'
import c from './c.mjs'
console.log(a, b, c);

Это удобно для подгрузки динамических ресурсов. Но, например, такое уже не сделать:
function diskUsage(dir) {
    return wait(fs.readdir(dir)).reduce((size, name) => {
        const sub = join(dir, name);
        const stat = wait(fs.stat(sub));
        if (stat.isDirectory()) return size + diskUsage(sub);
        else if (stat.isFile()) return size + stat.size;
        else return size;
    }, 0);
}

function printDiskUsage(dir) {
    console.log(`${dir}: ${diskUsage(dir)}`);
}

run(() => printDiskUsage(process.cwd()))
    .then(() => {}, err => { throw err; });

Добавлять сахар проще, чем вводить новую идиому в рантайм, это как сделать однопоточный js многопоточным, со всеми вытекающими.
Но проблему разделения мира на синхронную и асинхронную части никак не решить на async/await. Нельзя из синхронной функции вызывать асинхронную и работать с результатом.

Неправильный подход, вызовы будут выполнены последовательно
const authors = _.map(
authorIds,
id => await authorModel.fetch(id));

Этот код даже не запустится, ибо синтаксически нельзя использовать await в синхронных функциях. И вообще, что за _.map в 2k18?

В данном в статье примере запустится, если просто раскомментировать строки, так как фукнция getAuthors(authorIds) там async.
_.map это скорее всего lodash какой-нибудь, просто при переводе потеряли (либо автор в переводе посчитал это очевидным)

Не думаю что vintage забыл что такое lodash. Тут вопрос в другом: зачем писать _.map(authorIds, ..., когда давно уже можно написать authorIds.map(...?

Может быть там что-нибудь типа этого подключено (prefer-lodash-method). Поясняют они это так:


When using native functions like forEach and map, it's often better to use the Lodash implementation.

This can be for performance reasons, for implicit care of edge cases (e.g. _.map over a variable that might be undefined), or for use of Lodash's shorthands.
Есть еще способ обработки исключений и он мне больше нравится, но нужно включать поддержку декораторов:
@onError(e => -100) // invoke callback on error
async asyncFunctionCanThrowsError(error) {
    await ...
    throw new Error();
}
  
@defaultOnError(-1) // return default value (-1) on error
async defaultOnThrow(){
      await ...
      throw new Error();
}

Это не подходит прям для всего, но для простых случаев это более читабельно. Имхо.
Поиграться можно тут jsfiddle.net/jsbot/kw7py5r3

Очень сомнительный сахар


1) Теряется наглядность в последовательности исполнения. Сначала исполняется код снизу, потом верхняя часть.


2) Нет доступа к переменным из функции


@onError(e =>  fallbackFetchData(id)) // << где тут взять id?
async fetchData(id) {
   // code
}

3) Если обработчик будет из нескольких строк, то выигрыша по числу кода вообще нет


async asyncFunctionCanThrowsError(error) {
   try {
      // code
   } catch(error) {
      if(someCondition) {
         // some logic
      }
   }
}

против


@onError(error => {
   if(someCondition) {
      // some logic
   }
})
async asyncFunctionCanThrowsError(error) {
   // code
}

Хорошая попытка использовать декораторы, но работает только для однострочных функций. Если понадобится больше логики, придется переделывать все назад.

1. Это смотря какой подход, у меня подход исключений быть не должно 99.99% времени и в принципе исключение обозначает, что нужно показать юзеру «сервис недоступен» и делать с этим особо нечего. Поэтому код обработки пусть будет, но будет где то в сторонке, что бы не загрязнять основной код.

2. Конкретно это вообще не проблема, имя функции, аргументы, this и саму функцию можно передавать в callback
function onError(callback) {
...
   descriptor.value = function(...args) {
...
      return result.catch(e => callback(args, e)); // вот например передача аргументов
...
   }
}

@onError((args, errror)=>{})
...


3. Сделайте именованную функцию, но опять же это не универсальный способ и универсального способа нет, нужно смотреть по обстоятельствам. Общий принцип, все что упрощает код хорошо, все что повышает его читабельность, тоже хорошо и наоборот.
import handleError from 'errorHandler'

@onError(handleError)
async asyncFunctionCanThrowsError(error) {
   // code
}

Вот именно. Наворачиваются и добавляются усложениния, хотя можно просто обойтись try/catch

1. Только одна декларация намерения отлова исключений занимает минимум +3 строки и добавляет еще один отступ коду
try {
} catch() {
}

это много
2. Код обработки штатной ситуации и код обработки исключения обычно логически никак не связаны, смешивание их в одном месте, усложняет чтение.
Как решить эти проблемы, можно например было бы добавить сахар в язык что то вроде
function foo() {
   // logic
} catch(e) {
   // error handler
} 

Можно просто передать ошибку в место вызова кода, который дал сбой, и позволить обработать её там. Можно выбросить ошибку напрямую, воспользовавшись командой наподобие throw error;, что позволит использовать функцию async getBooksByAuthorWithAwait() в цепочке промисов. То есть, вызывать её можно будет, пользуясь конструкцией getBooksByAuthorWithAwait().then(...).catch(error => ...). Кроме того, можно обернуть ошибку в объект Error, что может выглядеть как throw new Error(error). Это позволит, например, при выводе сведений об ошибке в консоль, просмотреть полный стек вызовов.
Ошибку можно представить в виде отклонённого промиса, выглядит это как return Promise.reject(error). В данном случае это эквивалентно команде throw error, делать так не рекомендуется.


Тут автор уже немного перемудрил. Эти оба случая как раз эквивалентны по резултату если вообще не заключать await в try/catch.

Пробовал использовать в инициализации приложения mongodb, redis соединение к бд, роутеры, старт сервера, вроде и работает но откатил версию в коде назад на async.parallel
https://caolan.github.io/async/docs.html#parallel
Придерживаюсь правила, пока работает не трогай.
Пользуюсь только callback они быстрее, чем промисы работают. На большом количестве вызовов прирост ощутим.

Какая версия node.js? Испольуетели Вы трансфорамацию babel? Не должно быть на последних версиях принципиальной разницы. Хотя не так давно ббыли такие разговоры (сам не проверял) что использование try/catch в async функциях замедляло работу на порядок (раз в 20).
Вот правильный подход к написанию такого кода:
async getBooksAndAuthor(authorId) {
  const bookPromise = bookModel.fetchAll();
  const authorPromise = authorModel.fetch(authorId);
  const book = await bookPromise;
  const author = await authorPromise;
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}


Замечу что не обязательно писать именно так, обычно пишут вот такие конструкции, так как они проще:
async getBooksAndAuthor(authorId) {
  const [book, author] = await Promise.all([
    bookModel.fetchAll(),
    authorModel.fetch(authorId)
  ]);

  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}
В общем, имхо, с ES происходит то, что в него встраивают механизмы, которые позволяют достигнуть тех же целей, которые при разработке под ОС (или под JVM) достигаются многопоточностью. Но можно достигнуть их и через генераторы с yield и промисы. Следующий этап — наверное, должны начаться проблемы с синхронизацией всего этого хозяйства, и нужны будут аналоги критических секций, семафоров и мьютексов.

Критические секции не нужны в принципе именно благодаря тому что await действует только на один фрейм вызова: если в блоке кода нет оператора await посередине, то ни один "поток" не может прервать его выполнение.


Соответственно, асинхронные семафоры и мьютексы упрощаются до чего-то такого:


class AsyncMutex
{
    constructor() {
        this._promise = null;
        this._resolve = null;
    }

    async acquire() {
        while (this._promise)
            await this._promise;

        this._promise = new Promise(resove => this._resolve = resolve);
    }

    release() {
        if (!this._resolve)
            throw "Mutex is not acquired";
        this._resolve();
        this._promise = this._resolve = null;
    }
}
Можете подсказать(а то уже 2 дня мучаюсь)… как вернуть данные из функции?(сильно не пинайте… только столкнулся с промисами)

    async function f() {
      try {
        const query1 = budjet.find({}).sort({ $natural: -1 }).limit(1);//mongoose
        const result1 = await query1.exec();
        console.log(result1);// тут показывет нужные данные
        return result1;
      } catch (e) {
        console.error(e);
      }
    }

    console.log(f());// тут Promise { <pending> } 

заранее спасибо
f().then(data => console.log(data)); // 1-й вариант
console.log(await f()); // 2-й вариант

2-й вариант будет работать только в контексте вышестоящей тоже async-функции.

А чтобы не "мучиться ещё 2 дня" вслепую просто почитайте про Promise-ы и Async/await-ы. Вслепую ковыряться в них, надеясь разобраться из какого они теста, без знания теории, что за ними стоит, можно, но очень уж нерационально.

У вас функция объявлена с модификатором async, и она ожидаемо возвращает Promise. Соответственно, либо вы должны вызывать ее в контексте другой асинхронной функции, используя await, либо делать все операции с данными внутри .then

// inside another async function
async function yetAnotherFunction() {
  console.log(await f());
}

// particular cause: self-invoking function
(async () => {
    console.log(await f());
})();

// or
f().then((it) => {
  // do everything you want with result, e.g.:
  console.log(it);
});


P.S. пожалуйста, обратите внимание, что у вас функция возвращает либо массив объектов, либо undefined в зависимости от того, произошла ошибка или нет; лучше придерживаться единого формата ответа. Возможно имеет смысл обрабатывать ошибки на уровне выше.
P.P.S. если вам нужна только 1 запись, то лучше использовать findOne вместо find().limit(1)
P.P.P.S. Query в mongoose – это Promise-like объекты, поэтому вы можете использовать await прямо для них ;)
Sign up to leave a comment.