Я когда-то думал, что нет необходимости изучать 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 }
, итерация заканчивается.
Генераторы могут быть как итерируемыми, так и итераторами.
Есть два способа вернуть генератор:
- Создайте функцию генератора, используя конструктор функции генератора, функция генератора возвращает генератор, который редко используется на практике, и этот метод не рассматривается в этой статье.
- использовать
function*
Объявленная функция является функцией генератора, а функция генератора возвращает генератор
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
метод завершает работу генератора и возвращаетIteratorResult
,вdone
да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модуль;
- Нативно поддерживается с использованием Javascript
Promise
объект, сглаживающий асинхронный процесс, основанный на этой идее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, который также включает в себяшаблон итератора. В завершение прилагаются соответствующие справочные материалы, чтобы заинтересованные читатели могли продолжить изучение.