Почему рекурсивные асинхронные функции в Javascript приводят к переполнению стека? - PullRequest
4 голосов
/ 04 июля 2019

Рассмотрим этот фрагмент:

function f() {
  return new Promise((resolve, reject) => {
    f().then(() => {
      resolve();
    });
  });
}

f();

, который также можно записать так:

async function f() {
  return await f();
}

f();

Если вы запустите любой из указанных двух кодов, вы столкнетесь с этой ошибкой:

(node:23197) UnhandledPromiseRejectionWarning: RangeError: Maximum call stack size exceeded

У меня вопрос почему?Прежде чем ответить на мой вопрос, рассмотрите мой аргумент:

Я понимаю концепцию рекурсии и то, как она приводит к переполнению стека, если нет условия остановки.Но мой аргумент здесь заключается в том, что после выполнения первого f(); он вернет Promise и выйдет из стека, поэтому эта рекурсия не должна сталкиваться с переполнением стека.Для меня это должно вести себя так же, как:

while (1) {}

Конечно, если я напишу это так, это будет исправлено:

function f() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      f().then(() => {
        resolve();
      });
    }, 0);
  });
}

f();

, что является другой историей, и яУ меня нет проблем с этим.

[ОБНОВЛЕНИЕ]

Мое плохое, я забыл упомянуть, что тестировал с node v8.10.0 на стороне сервера.

Ответы [ 2 ]

5 голосов
/ 04 июля 2019

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

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

По ссылке выше

Функция executor выполняется немедленно реализацией Promise, передавая функции разрешения и отклонения (исполнитель вызывается до того, как конструктор Promise даже возвращает созданный объект).

1 голос
/ 04 июля 2019

Благодаря @Adrian мне удалось найти способ избежать переполнения стека. Но до этого он был прав, и эта форма рекурсии должна была привести к переполнению стека. И поскольку вопрос «почему», его ответ принят. Это моя попытка «как» не сталкиваться с переполнением стека.

Тест 1

function f() {
  return new Promise((resolve) => {
    resolve();
  }).then(f);
}

И используя await:

Тест 2

async function f() {
  return await Promise.resolve()
    .then(f);
}

Я не уверен, можно ли в этом случае устранить Promise!

И я знаю, что сказал нет setTimeout, но это тоже интересный случай:

Тест 3

async function f() {
  await new Promise(resolve => setTimeout(resolve, 0));
  return f();
}

Это также не столкнется с переполнением стека.

В конце, чтобы дать вам контекст, почему я был заинтересован в этом; допустим, вы кодируете функцию для извлечения всех записей из DynamoDb AWS. Поскольку существует ограничение на количество записей, которые вы можете извлечь из DynamoDb за один запрос, вы должны отправить столько записей, сколько необходимо (с ExclusiveStartKey), чтобы получить все записи:

Тест 4

async function getAllRecords(records = [], ExclusiveStartKey = undefined) {
    let params = {
        TableName: 'SomeTable',
        ExclusiveStartKey,
    };

    const data = await docClient.scan(params).promise();
    if (typeof data.LastEvaluatedKey !== "undefined") {
        return getAllRecords(records.concat(data.Items), data.LastEvaluatedKey);
    }
    else {
        return records.concat(data.Items);
    }
}

Я хотел убедиться, что это никогда не столкнется с переполнением стека. Было нереально иметь такую ​​огромную таблицу DynamoDb, чтобы на самом деле это проверить. Поэтому я придумал несколько примеров, чтобы убедиться в этом.

Сначала казалось, что тест № 4 действительно может столкнуться с переполнением стека, но мой тест № 3 показывает, что такой возможности нет (из-за await docClient.scan(params).promise()).

[UPDATE]

Спасибо @Bergi, вот код для await без Promise:

Тест 5

async function f() {
  await undefined;
  return f();
}
...