Галоўная / Артыкулы / Адказанне пра генераторах JavaScript: функцыі, якія можна націснуць, і ледзючая ітерацыя

Адказанне пра генераторах JavaScript: функцыі, якія можна націснуць, і ледзючая ітерацыя

Дазвольце дазнацься, як функцыі-генераторы JavaScript зупіняюць і практыкуюць выконанне, рэалізуюць протакол ітэратора і дазволяюць атрыбутны, эфектываўшы па выкарыстоўванні памяці строеў працы з дадзеннямі.

1084 слоў

Кожная функцыя, якую вы напісалі даўна, павінна адпавядаць ад однаго правіла: вы яе вызываеце, яна выконваецца з пачатку да канца, вяртае рэзультат адно раз і на гэтым усё. Генераторы таямніча наражаюцца на гэта правіло, і гэта наражанне ў практыцы мае вельмі значныя наследкі: функцыя-генератор можа перываць свою роботу на паловым шляху, перадаць значэнне таму, хто яе вызваў, і пазней продаваць роботу з таго самага месца, нібы час усёга не прыйшоў.

Базавы механізм

Генератор адзначаецца за дапамою function*, і замест return ён выкарыстоўвае yield, каб паштоўна перадаваць значэнні:

function* countUp() {
  console.log("starting");
  yield 1;
  console.log("resumed after first yield");
  yield 2;
  console.log("resumed after second yield");
  yield 3;
  console.log("done");
}
const counter = countUp();
console.log(counter.next()); // "starting" logs, then { value: 1, done: false }
console.log(counter.next()); // "resumed after first yield" logs, then { value: 2, done: false }
console.log(counter.next()); // "resumed after second yield" logs, then { value: 3, done: false }
console.log(counter.next()); // "done" logs, then { value: undefined, done: true }

Выкліканне countUp() не запускае нічога з тэлу функцыі. Яна вяртае об’ект-генератор, спячуючы элемент, які ўсё ще не запускаўся. Кожны наступны выклік .next() продовжывае виконанне там, дзе яно было зупінена, працюе да наступнага yield і знову зупіняецца, вяртаючы значэнне, якое было выдана. Усі внутрашняя стан функцыі, яе зменны, ўзгоднасць з цыклам, будзь-што інше залишаецца недыягнальным праз гэтыя перерывы, адночасна з тым, як будзь-гад кантроль над функцыяй не быў перарваны.

Чаму гэта справжна разлічаецца ад простага вяртання масіву

Натуральныя заперкі: чаму проста не створыць масэў і не вернуць яе замест? Сюды-то і падходзяць генератары, адколі яны выдаюць данні поступова, па аднае елементы, толькі калі гэта запатребавана, а не вырахоўваюць весь набор рэзультата заздалегідь.

function* readLargeFileLineByLine(filePath) {
  const fileHandle = fs.openSync(filePath, "r");
  let position = 0;
  let leftover = "";
  while (true) {
    const buffer = Buffer.alloc(1024);
    const bytesRead = fs.readSync(fileHandle, buffer, 0, 1024, position);
    if (bytesRead === 0) break;    position += bytesRead;
    const lines = (leftover + buffer.toString("utf8", 0, bytesRead)).split("\n");
    leftover = lines.pop();
    for (const line of lines) yield line;
  }
  fs.closeSync(fileHandle);
}for (const line of readLargeFileLineByLine("access.log")) {
  if (line.includes("500")) console.log(line);
  // only reads and holds one small chunk of the file in memory at a time
}

Якбы код быў напісан так, каб з самага пачатку сформаваць масэў усіх ліній, то файл журналу паўгігабайтовага размеру быў бы неабходны прыняць у памяцю цэлы, перш чым можна было б запрацаваць з хоць адной лініяй, і гэта тая ж проблема, якой прагнуць ухіліцца за дапамогою стрімінгавых падходаў. Генератар ухіляецца ад гэтага за дапамогою іншага механізма: кожная лінія вырахоўвается толькі тады, калі ёй запатребаваюць за дапамогою .next(), і якщо суседня цыклічная структура for...of завершаецца раней, напрыклад, калі знайдзе тое, што шукала, генератар нават не прабуе вырахоўваць што-небудзь па захаду таго моменту.

Генератары рэалізуюць протакол ітэратора, таму for...of проста працюе

Структуры на кшталт for...of, дэструктурыроўання масів і аператара распрашчэння працуюць з усім, што падтрымляе протакол ітэратора, тыябо з любым об’ектам, які выканае метод next(), який вяртае { value, done }. Об’ект генератара аўтаматычна падходзіць пад гэты формат без дапамоги ўжо, што пояснюе, чаму можна без дадатковых налаштаванняў ітэратаваць яго безпасова:

function* pageThroughResults(fetchPage) {
  let page = 1;
  while (true) {
    const results = fetchPageSync(fetchPage, page);
    if (results.length === 0) return;
    yield* results; // delegates to another iterable, yielding each of its values
    page++;
  }
}

Синтаксіс yield* перадае задачу іншаму ітэрабельнаму об’екту, перадаючы кожную з яго значэнняў па спрабе. Самэ так генератар можа ператворыць серію результатаў з пагінацыяй у адну неперывную стрэму окремых элементаў, пры чым код, які яе выкарыстоўвае, ніколі не павінен ведаць пра тое, што пад час выканання відбываецца пагінацыя.

Двухканальная абстракцыя: .next() можа выдаваць значэнні, а не толькі іх запрашоваць

У генератораў ёсць менш вядомы аспект: yield — гэта не толькі спосаб выдачы значэнняў, але і выраз; все, што вы пасылаеце ў наступны вызов .next(), стае рэзультатам гэтага выраза. Адным словам, даныя можу вярнуцца ў функцію, якая знаходзится у стане паузы, а не толькі выйсці з яе.

function* priceNegotiation() {
  const offer1 = yield "What's your offer?";
  const offer2 = yield `I can't do ${offer1}, how about a counter?`;
  return `Final: ${offer2}`;
}
const negotiation = priceNegotiation();
console.log(negotiation.next().value);        // "What's your offer?"
console.log(negotiation.next(50).value);       // "I can't do 50, how about a counter?"
console.log(negotiation.next(80).value);       // "Final: 80"

Кожны раз, калі вы вызываеце .next(value), генератор прыходзіць у робочы стан, і адмовленая лінія yield выконваецца з паданым вам value. Няверагодная для звычнай разработкі функцый спроможнасць — гэта можласць павярнуць виконанне, чакаць на новы вхідны данні ад вызываючага і пасля прыняць роботу з гэтымі данніма. Саме на такі механізм спаляліся раннія бібліятэкі JavaScript для асінхронной роботы, каб імітаваць async/await, калі язык яшчэ не падтрымляў іх натывна: скаржыбнік рашаў задачу, а пасля перадаваў яе рэзультат у генератор через .next(), павтараючы гэта за кожным yield, пакуль не будзе завершана кожная асінхронная дзейнасць.

Чаму гэта ёсць фундамент, які варта знать

Генератары здаецца не синтаксічныя абэррацыі, якія можна безпечна ігнараваць. Яны ўсё той механізм, на які зрэшты былы паставлены async/await, адколі «пазначыць паузу ў выконанні і продаважыць яе пазней з якім-небудзь значэнням» — гэта саме та поведінка, якой патрабуе абсчыканне на обяцанню. Яны таксама поясняюць, чырэз што for...of, синтаксіс розпашчання та деструктурырацыя практычна аднакова працуюць з масавымі, стрэлкамі, экземплярамі Map і спецыяльнымі об’ектамі, адколі протакол ітэратара, які реалізуюць генератары, ідэнтычны таму, на які вже спалююцяся гэтыя вбудованыя типы. Калі вы усваяеце справжнюй ментальны модэль — функцыю, якая можа паважваць та продаважыць выконанне, адмаўляючы значэння ў кожны момент паузы, ледзьве-ледзь абсчыканне, спецыяльную логіку ітэрацыі та внутршняе функцыонаванне async/await — гэтыя элементы больш не здаюцца трома незв’язанымі функцыямі. Адкрыўаецца, што яны ўсё-такі ўзаемна пов’язаны.

Асасны патэрн, які праявляецца ў трох аднародных контэкстах.

Спадні матэрыялы