Расширенный генератор Javascript

внешний интерфейс JavaScript React.js RxJS

Я когда-то думал, что нет необходимости изучать JavascriptГенератор ( Генератор ), что это всего лишь переходное решение для устранения асинхронного поведения, и только после недавнего углубленного изучения связанных библиотек инструментов я постепенно осознал его мощь. Возможно, вы не написали генератор вручную, но вы должны отрицать, что он широко используется, особенно вredux-sagaа такжеRxJSи другие отличные инструменты с открытым исходным кодом.


Итерации и итераторы

Первое, что нужно уяснить, это то, что генераторы на самом деле являются производными от шаблона проектирования —Итечерский режим, представление шаблона итератора в Javascript таково:Итерируемый протокол, и именно отсюда в ES2015 берутся итераторы и итерируемые объекты, два запутанных понятия, но на самом деле ES2015 проводит между ними четкое различие.

определение

ДостигнутоnextМетод объекта называетсяИтератор.nextметод должен возвращатьIteratorResultобъект, который выглядит так:

{ value: undefined, done: true }

вvalueпредставляет результат этой итерации,doneУказывает, завершена ли итерация.

Достигнуто@@iteratorОбъект метода называетсяповторяемый объект, то есть объект должен иметь имя,[Symbol.iterator]свойство, это свойство является функцией, возвращаемое значение должно бытьитератор.

String, Array, TypedArray, Mapа такжеSetэто итерируемый объект, встроенный в Javascript, например,Array.prototype[Symbol.iterator]а такжеArray.prototype.entriesвернет тот же итератор:

const a = [1, 3, 5];
a[Symbol.iterator]() === a.entries();   // true
const iter = a[Symbol.iterator]();      // Array Iterator {}
iter.next()                             // { value: 1, done: false }

Новая деструктуризация массива в ES2015 также использует итераторы для итерации по умолчанию:

const arr = [1, 3, 5];
[...a];                        // [1, 3, 5]
const str = 'hello';
[...str];                      // ['h', 'e', 'l', 'l', 'o']

Пользовательское поведение итерации

Поскольку итерируемый объект реализован@@iteratorметод, то итерируемый объект может быть переопределен с помощью@@iteratorМетод реализует пользовательское поведение итерации:

const arr = [1, 3, 5, 7];
arr[Symbol.iterator] = function () {
  const ctx = this;
  const { length } = ctx;
  let index = 0;
  return {
    next: () => {
      if (index < length) {
        return { value: ctx[index++] * 2, done: false };
      } else {
        return { done: true };
      }
    }
  };
};

[...arr];       // [2, 6, 10, 14]

Как видно из вышеизложенного, когдаnextметод возвращает{ done: true }, итерация заканчивается.

Генераторы могут быть как итерируемыми, так и итераторами.

Есть два способа вернуть генератор:

const counter = (function* () {
  let c = 0;
  while(true) yield ++c;
})();

counter.next();   // { value: 1, done: false },counter 是一个迭代器

counter[Symbol.iteratro]();
// counterGen {[[GeneratorStatus]]: "suspended"}, counter 是一个可迭代对象

в коде вышеcounterЭто генератор, который реализует простую функцию подсчета. Он не только не использует замыкания или глобальные переменные, но и реализован очень элегантно.


Базовый синтаксис для генераторов

Сила генераторов в том, что они могут легко управлять логикой внутри функции генератора. Внутри функции генератора,yieldилиyield*, который передает управление текущей функцией генератора вовне, вызывающему функцию генератораnextилиthrowилиreturnМетод возвращает управление функции-генератору, а также может передавать ей данные.

выражения yield и yield*

yieldа такжеyield*Может использоваться только в функциях генератора. Внутри функции генератора черезyieldВозвращаясь рано, предыдущий счетчик должен использовать эту функцию для передачи результата подсчета вовне. Обратите внимание, что предыдущий счетчик выполняется бесконечно, пока генератор вызываетnextметод,IteratorResultизvalueОн будет продолжать увеличиваться.Если вы хотите подсчитать конечное значение, вам нужно использовать его в функции генератораreturnвыражение:

const ceiledCounter = (function* (ceil) {
  let c = 0;
  while(true) {
    ++c;
    if (c === ceil) return c;
    yield c;
  }
})(3);

ceiledCounter.next();   // { value: 1, done: false }
ceiledCounter.next();   // { value: 2, done: false }
ceiledCounter.next();   // { value: 3, done: true }
ceiledCounter.next();   // { value: undefined, done: true }

yieldможно следовать без каких-либо выражений, возвращаемыйvalueдляundefined:

const gen = (function* () {
  yield;
})();
gen.next();    // { value: undefined, done: false }

функция генератора с использованиемyield*Выражения используются для делегирования другому итерируемому объекту, включая генераторы.

Делегат встроенных итераций Javascript:

const genSomeArr = function* () {
  yield 1;
  yield* [2, 3];
};

const someArr = genSomeArr();
greet.next();   // { value: 1, done: false }
greet.next();   // { value: 2, done: false }
greet.next();   // { value: 3, done: false }
greet.next();   // { value: undefined, done: true }

Делегировать другому генератору (все еще используя вышеуказанныйgenGreetфункция генератора):

const genAnotherArr = function* () {
  yield* genSomeArr();
  yield* [4, 5];
};

const anotherArr = genAnotherArr();
greetWorld.next();   // { value: 1, done: false}
greetWorld.next();   // { value: 2, done: false}
greetWorld.next();   // { value: 3, done: false}
greetWorld.next();   // { value: 4, done: false}
greetWorld.next();   // { value: 5, done: false}
greetWorld.next();   // { value: undefined, done: true}

yieldВыражения имеют возвращаемые значения, и далее объясняется конкретное поведение.

next , методы throw и return

Именно с помощью этих трех методов функция-генератор управляет внутренним процессом выполнения функции-генератора.

next

вне функции генератора можетnextМетоду передается параметр, этот параметр будет рассматриваться как предыдущийyieldВозвращаемое значение выражения, если аргументы не переданы,yieldвыражение возвращаетundefined:

const canBeStoppedCounter = (function* () {
  let c = 0;
  let shouldBreak = false;
  while (true) {
    shouldBreak = yield ++c;
    console.log(shouldBreak);
    if (shouldBreak) return;
  }
};

canBeStoppedCounter.next();
// { value: 1, done: false }

canBeStoppedCounter.next();
// undefined,第一次执行 yield 表达式的返回值
// { value: 2, done: false }

canBeStoppedCounter.next(true);
// true,第二次执行 yield 表达式的返回值
// { value: undefined, done: true }

Давайте рассмотрим пример непрерывной передачи значений:

const greet = (function* () {
  console.log(yield);
  console.log(yield);
  console.log(yield);
  return;
})();
greet.next();        // 执行第一个 yield表达式
greet.next('How');   // 第一个 yield 表达式的返回值是 "How",输出 "How"
greet.next('are');   // 第二个 yield 表达式的返回值是 "are",输出"are"
greet.next('you?');  // 第三个 yield 表达式的返回值是 "you?",输出 "you"
greet.next();        // { value: undefined, done: true }

throw

вне функции генератора можетthrowМетоду передается параметр, этот параметр будетcatchзахват оператора, если аргументы не переданы,catchЗахваченное заявление будетundefined,catchПосле захвата оператора выполнение генератора возобновляется, возвращаясь сIteratorResult:

const caughtInsideCounter = (function* () {
  let c = 0;
  while (true) {
    try {
      yield ++c;
    } catch (e) {
      console.log(e);
    }
  }
})();

caughtInsideCounter.next();    // { value: 1, done: false}
caughtIndedeCounter.throw(new Error('An error occurred!'));
// 输出 An error occurred!
// { value: 2, done: false }

Следует отметить, что если нет внутренней генераторной функцииcatchк, это будет внешнеcatchк, если внешнее тоже не имеетcatch, это приведет к прекращению выполнения программы, как и в случае всех неперехваченных ошибок:

return

генераторreturnметод завершает работу генератора и возвращаетIteratorResultdoneдаtrue,valueДreturnПараметры, переданные методом, если параметры не переданы,valueбудетundefined:

const g = (function* () { 
  yield 1;
  yield 2;
  yield 3;
})();

g.next();        // { value: 1, done: false }
g.return("foo"); // { value: "foo", done: true }
g.next();        // { value: undefined, done: true }

С помощью трех вышеуказанных методов внешняя часть функции-генератора имеет очень сильный контроль над внутренним процессом выполнения программы функции-генератора.


Асинхронное применение генераторов

Генераторные функции в сочетании с асинхронными операциями представляют собой очень естественные выражения:

const fetchUrl = (function* (url) {
  const result = yield fetch(url);
  console.log(result);
})('https://api.github.com/users/github');

const fetchPromise = fetchUrl.next().value;
fetchPromise
  .then(response => response.json())
  .then(jsonData => fetchUrl.next(jsonData));

// {login: "github", id: 9919, avatar_url: "https://avatars1.githubusercontent.com/u/9919?v=4", gravatar_id: "", url: "https://api.github.com/users/github", …}

В приведенном выше кодеfetchметод возвращаетPromiseобъектfetchPromise,fetchPromiseПосле серии парсингов он вернет объект в формате JSON.jsonData, пройти черезfetchUrlизnextметод, переданный функции-генераторуresult, а затем распечатайте его.

Ограничения генераторов

Как видно из приведенного выше процесса, генератор взаимодействуетPromiseЭто правда, что асинхронные операции можно выполнять очень лаконично, но этого недостаточно, потому что весь асинхронный процесс пишется вручную. Когда асинхронное поведение становится более сложным (например, очередь асинхронных операций), процесс управления асинхронным процессом генератора также становится трудным для написания и обслуживания.

Генераторы действительно пригодятся, когда вам нужен инструмент, автоматизирующий асинхронные задачи. Обычно существует два способа реализации такого инструмента:

  • Непрерывно выполняя функцию обратного вызова до тех пор, пока весь процесс не будет завершен, на основе этой идеиthunkifyмодуль;
  • Нативно поддерживается с использованием JavascriptPromiseобъект, сглаживающий асинхронный процесс, основанный на этой идееcoмодуль;

Давайте разберемся и реализуем их по отдельности.

thunkify

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

пониматьthunkifyПоразмыслив, его можно сократить до упрощенной версии (только для понимания, а не для производства):

const thunkify = fn => {
  return (...args) => {
    return callback => {
      return Reflect.apply(fn, this, [...args, callback]);
    };
  };
};

Как видно из приведенного выше кода,thunkifyФункция, применяемая к функции обратного вызова, является асинхронной функцией последнего параметра.Здесь мы создаем асинхронную функцию, которая соответствует этому стилю для легкой отладки:

const asyncFoo = (id, callback) => {
  console.log(`Waiting for ${id}...`)
  return setTimeout(callback, 2000, `Hi, ${id}`)
};

Первый — основное использование:

const foo = thunkify(asyncFoo);
foo('Juston')(greetings => console.log(greetings));
// Waiting for Juston...
// ... 2s later ...
// Hi, Juston

Затем мы моделируем фактический спрос и выводим результаты каждые 2 с. Во-первых, построить функцию генератора:

const genFunc = function* (callback) {
  callback(yield foo('Carolanne'));
  callback(yield foo('Madonna'));
  callback(yield foo('Michale'));
};

Далее реализуем вспомогательную функцию, которая автоматически запускает генераторrunGenFunc:

const runGenFunc = (genFunc, callback, ...args) => {
  const g = genFunc(callback, ...args);

  const seqRun = (data) => {
    const result = g.next(data);
    if (result.done) return; 
    result.value(data => seqRun(data));
  }

  seqRun();
};

Уведомлениеg.next().valueявляется функцией и принимает функцию обратного вызова в качестве параметра,runGenFuncДва ключевых шага выполняются с кодом в строке 7:

  • поставить предыдущийyieldГенераторная функция, которая возвращает результат выражения
  • выполнить текущийyieldвыражение

Наконец звонокrunGenFuncи воляgenFunc, функция обратного вызова, которую необходимо использоватьcallbackИ другие параметры функции генератора (здесь функция генератора имеет только одну функцию обратного вызова в качестве параметра):

runGenFunc(genFunc, greetings => console.log(greetings));
// Waiting for Carolanne...
// ... 2s later ...
// Hi, Carolanne
// Waiting for Madonna...
// ... 2s later ...
// Hi, Madonna
// Waiting for Michale...
// ... 2s later ...
// Hi, Michale

Видно, что результаты вывода действительно такие, как ожидалось, и вывод выполняется каждые 2 с.

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

co

сомодульЭто может помочь нам завершить автоматическое выполнение асинхронных процессов.coмодуль основан наPromiseобъект.coИсходный код модуля также очень лаконичен и удобен для чтения.

coЕсть только два API для модулей:

  • co(fn*).then(val => )

    coМетод принимает функцию-генератор в качестве единственного параметра и возвращаетPromiseобъект, основное использование выглядит следующим образом:

    const promise = co(function* () {
      return yield Promise.resolve('Hello, co!');
    })
    promise
      .then(val => console.log(val))   // Hello, co!
      .catch((err) => console.error(err.stack));
    
  • fn = co.wrap(fn*)

    co.wrapметод вcoМетод дополнительно упаковывается на основе возврата аналогичногоcreatePromiseфункция, аналогичнаяcoОтличие метода в том, что он может передавать параметры во внутреннюю функцию-генератор.Основное использование заключается в следующем.

    const createPromise = co.wrap(function* (val) {
      return yield Promise.resolve(val);
    });
    createPromise('Hello, jkest!')
      .then(val => console.log(val))   // Hello, jkest!
      .catch((err) => console.error(err.stack));
    

coмодуль требует от насyieldОбъект после ключевого слова преобразуется вcoмодуль пользовательскийyieldableобъект, который обычно можно рассматривать какPromiseобъекта или на основеPromiseСтруктура данных объекта.

понялcoПосле использования модуля написать не сложноcoПоток автоматического выполнения модуля.

просто нужно переделатьasyncFooфункция для возвратаyieldableобъект, в данном случаеPromiseОбъект:

const asyncFoo = (id) => {
    return new Promise((resolve, reject) => {
      console.log(`Waiting for ${id}...`);
      if(!setTimeout(resolve, 2000, `Hi, ${id}`)) {
        reject(new Error(id));
      }
    });
};

Затем вы можете использоватьcoмодуль для вызова, так как он долженgenFuncфункция проходит вcallbackпараметры, поэтому вы должны использоватьco.wrapметод:

co.wrap(genFunc)(greetings => console.log(greetings));

Приведенные выше результаты соответствуют ожиданиям.

фактическиcoВнутренняя реализация модуля аналогичнаthunkifyв разделеrunGenFuncФункции имеют одинаковую цель, все они используют рекурсивные функции для выполнения неоднократноyieldоператора, до конца итерации функции генератора, основное отличие состоит в том, чтоcoмодуль основан наPromiseосуществленный.


Возможно использование внешних модулей для выполнения соответствующих функций в большей части фактической работы, но если вы хотите понять принцип реализации или не хотите обращаться к внешним модулям, очень важно глубоко понимать использование генераторов. В следующей статье[Применение шаблона наблюдателя в Javascript], я рассмотрю принцип реализации RxJS, который также включает в себяшаблон итератора. В завершение прилагаются соответствующие справочные материалы, чтобы заинтересованные читатели могли продолжить изучение.


использованная литература