generators and iterators - garevna/js-course GitHub Wiki
:mortar_board: Генераторы и итераторы
ES6
:mortar_board: Генератор
Функция-генератор объявляется с помощью ключевого слова function*
:warning: * - обязательный атрибут функции-генератора
Функция-генератор определяет порядок ( протокол ) итерирования структуры данных
Сама по себе функция-генератор ничего не итерирует
С помощью функции-генератора создается объект-итератор
let iterator = generator ( ... )
Она ничего не возвращает, кроме, как уже было сказано, объекта-итератора с заложенным в нем протоколом перебора значений
:warning: Именно поэтому генератор вместо оператора return использует оператор yield
function* generator ( ... ) {
...
yield ...
}
Оператор yield позволяет управлять работой итератора
Этим оператором функция-генератор говорит итератору, что в этом месте нужно остановиться и вернуть текущее значение
:mortar_board: next()
У объекта-итератора есть обязательный метод next()
С помощью этого метода итератор переходит от текущего элемента структуры данных к следующему
:warning: Этот метод возвращает объект с двумя свойствами: value и done
- Свойство
valueсодержит то, что указано в протоколе генератора после ключевого слова словаyield - Свойство
doneпринимает значениеtrue, когда процесс итерирования структуры данных завершен
:coffee: 1
function* colorsGenerator () {
while ( true ) {
yield `rgb(
${Math.round ( Math.random() * 255 )},
${Math.round ( Math.random() * 255 )},
${Math.round ( Math.random() * 255 )}
)`
}
}
let colorIterator = colorsGenerator ()
for ( var x=0; x < 100; x++ ) {
let point = document.body.appendChild (
document.createElement ( 'div' )
)
point.style = `
float: left;
width: 10px;
height: 10px;
background-color: ${ colorIterator.next().value};
`
}
Используя IIFE, можно сократь код:
const colorIterator = ( function* () {
while ( true ) {
yield `rgb(
${Math.round ( Math.random() * 255 )},
${Math.round ( Math.random() * 255 )},
${Math.round ( Math.random() * 255 )}
)`
}
})()
Также можно отдельно вынести в функцию код создания элемента с параметрами ширины и высоты:
function createColoredElement ( w, h ) {
let point = document.createElement ( 'div' )
point.style = `
position: absolute;
width: ${w}px;
height: ${h}px;
background-color: ${ colorIterator.next().value};
`
return point
}
После чего можно в цикле создавать элементы:
for ( var x = 0; x < 75; x++ ) {
document.body.appendChild (
createColoredElement ( 400-x*5, 400-x*5 )
)
}
:coffee: 2
Для бесконечной подзагрузки данных с сервера при прокрутке страницы можно также использовать итератор:
const iterator = (
function* ( arg ) {
while ( true ) {
yield fetch ( `https://api.github.com/users?since=${arg}` )
.then (
response => response.json()
.then (
response => response.forEach (
user => {
let img = document.body.appendChild (
document.createElement ( "img" )
)
img.src = user.avatar_url
img.height = 100
}
)
)
)
arg += 30
}
}
)(0)
document.body.onmousewheel = function ( event ) {
iterator.next()
}
:coffee: 3
Пусть есть некий объект user
let user = {
login: "Сергей",
avatar: "https://www.shareicon.net/data/2015/12/14/207817_face_300x300.png",
email: "[email protected]",
place ( tagName ) {
return document.body.appendChild (
document.createElement ( tagName )
)
},
showAvatar () {
let ava = this.place ( "img" )
ava.src = this.avatar
ava.width = "70"
return ava
},
showLogin () {
let x = this.place ( "h3" )
.innerHTML = this.login
return x
},
showEmail () {
let x = this.place ( "p" )
.innerHTML = this.email
return x
}
}
С помощью генератора определим протокол итерирования этого объекта:
user.generator = function* () {
yield this.showLogin ()
yield this.showEmail ()
yield this.showAvatar ()
}
Теперь создадим объект итератора:
user.iterator = user.generator ()
и запустим цикл итерирования:
while ( !user.iterator.next().done ) {}
На самом деле такое решение является чрезмерно громоздким
Все значительно упростится с использованием глобального символа Symbol.iterator
:mortar_board: Symbol.iterator
Все очень просто:
Если у объекта есть свойство Symbol.iterator, то этот объект является итерабельным
( то есть можно перебирать его свойства оператором for...of )
Symbol.iterator является ссылкой на функцию-генератор
Используем Symbol.iterator в контексте предыдущего примера
user [ Symbol.iterator ] = function* () {
yield this.showLogin ()
yield this.showEmail ()
yield this.showAvatar ()
}
Теперь объект user можно итерировать обычным for...of
for ( var x of user ) {}
или воспользоваться оператором spread:
console.log ( ...user )
:coffee: 4
const elements = [
{ tagName: "div", attrs: { id: "first", innerText: "first" } },
{ tagName: "article", attrs: { id: "second", innerText: "second" } },
{ tagName: "figure", attrs: { id: "third", innerText: "third" } },
{ tagName: "p", attrs: { id: "forth", innerText: "forth" } }
]
elements [ Symbol.iterator ] = function* () {
let itemNum = 0
while ( itemNum < this.length ) {
yield ( () => {
var elem = document.body.appendChild (
document.createElement (
this [ itemNum ].tagName
)
)
if ( this [ itemNum ].attrs )
for ( var x in this [ itemNum ].attrs ) {
elem [ x ] = this [ itemNum ].attrs [ x ]
}
itemNum++
return elem
})()
}
}
for ( let elem of elements ) {}
:mortar_board: Асинхронный генератор
:coffee: 5
Создадим генератор, который выдает по одному символу в секунду из массива, переданного ему в качестве аргумента
async function* messageGenerator ( arr ) {
while ( arr.length > 0 ) {
let result = await new Promise (
function ( resolve ) {
setTimeout (
() => resolve ( arr.shift() ),
1000
)
}
)
yield result
}
}
Поскольку протокол итерирования, заложенный в генераторе, возвращает промис на каждой итерации, для работы с ним объявим асинхронную функцию showMessage
showMessage создаст итератор с помощью генератора messageGenerator, передав ему строку, которая будет выводиться на страницу по одному символу в секунду
showMessage будет ждать ( await ), когда асинхронный итератор вернет очередное значение, и после этого выведет его на страницу
async function showMessage ( message ) {
const iterator = messageGenerator ( [...message] )
let finish = false
while ( !finish ) {
let currentState = await iterator.next()
document.body.innerText += !currentState.done ?
currentState.value : ""
finish = currentState.done
}
}
Вызовем асинхронную функцию showMessage:
showMessage ( "Привет, студент!" )
:coffee: 6
Асинхронный генератор
let circle = document.createElement ( "div" )
circle.style = `
border: solid 2px blue;
width: 50px;
height: 50px;
position: absolute;
border-radius: 50%;
transition: all 0.2s;
opacity: 1;
`
circle.bubblesGenerator = ( async function* () {
let bubble = () => new Promise (
function ( resolve ) {
setTimeout ( () => resolve ( "next" ), 100 )
}
)
while ( true ) {
let radius = this.offsetWidth > 200 ?
50 : this.offsetWidth + 5
await bubble ()
this.style.width = `${radius}px`
this.style.height = `${radius}px`
this.style.opacity = radius === 50 ?
1 : Math.max ( this.style.opacity - 0.02, 0 )
yield radius
}
}).call ( circle )
document.body.appendChild ( circle )
async function show () {
let step = 200
while ( step --> 0 )
await circle.bubblesGenerator.next()
}
show()
:coffee: 7
Метод entries() объекта headers ответа сервера возвращает итератор
Воспользуемся этим для вывода в консоль всех заголовков ответа
async function getHeaders ( url ) {
let response = await fetch ( url )
let iterator = response.headers.entries()
do {
var { done: stop, value: header } = iterator.next()
header ? console.log ( `${header[0]}: ${header[1]}` ) : null
} while ( !stop )
}
Вызовем функцию
getHeaders ( 'https://api.github.com/users/5' )
и увидим в консоли следующее:
cache-control: public, max-age=60, s-maxage=60
content-type: application/json; charset=utf-8
etag: W/"7870416c9818dd4ba65ab505535c7b79"
last-modified: Fri, 28 Dec 2018 06:04:01 GMT
x-github-media-type: github.v3; format=json
x-ratelimit-limit: 60
x-ratelimit-remaining: 58
x-ratelimit-reset: 1560744850
Примеры асинхронного генератора
| :coffee: 8 | :coffee: 9 | :coffee: 10 | :coffee: 11 |
|---|
:coffee: 11 Описание примера
:mortar_board: Связные списки
:coffee: 12
Пусть у нас есть массив объектов
const objects = [
{ val: "first", nextItem: "second" },
{ val: "forth", nextItem: "fifth" },
{ val: "sixth", nextItem: null },
{ val: "third", nextItem: "forth" },
{ val: "fifth", nextItem: "sixth" },
{ val: "second", nextItem: "third" }
]
Каждый элемент массива содержит свойство nextItem - ссылку на другой элемент этого же массива
Создадим протокол итерирования такого массива
Пусть элементы массива перебираются не в том порядке, в котором они расположены в массиве, а по новому протоколу, т.е. следующим будет выбираться элемент, указанный в свойстве nextItem текущего элемента
function* someGenerator ( objs ) {
let currentItem = objs [ 0 ]
let nextItem = objs [ 0 ]
while ( !!nextItem ) {
currentItem = nextItem
nextItem = !!currentItem.nextItem ?
objs.find ( x => currentItem.nextItem === x.val )
: null
yield currentItem.val
}
}
Генератор принимает в качестве аргумента ссылку на итерируемый массив
Создадим итератор для массива objects
var iterator = someGenerator ( objects )
Теперь можно использовать метод next() итератора iterator
:pushpin: Изменим протокол итерирования массива
objects[Symbol.iterator] = function* () {
let currentItem = this [ 0 ]
let nextItem = this [ 0 ]
while ( !!nextItem ) {
currentItem = nextItem
nextItem = !!currentItem.nextItem ?
this.find ( x => currentItem.nextItem === x.val )
: null
yield currentItem.val
}
}
Теперь оператор for...of будет итерировать массив objects в нужном порядке
for ( let obj of objects )
console.log ( obj )
Кроме того, при деструктуризации массива objects значения будут возвращены в указанном протоколом порядке
let [ a, b, c, d ] = objects
:coffee: 13
let btn = document.body.appendChild (
document.createElement ( "button" )
)
btn.innerText = "new"
btn.onclick = function ( event ) {
let ava = getAvatar.next()
if ( !ava.done ) document.body.appendChild ( ava.value )
}
function* avaGenerator () {
let num = 9
while ( ++num < 99 ) {
let ava = document.createElement ( "img" )
ava.src = `https://www.shareicon.net/data/2015/12/14/2078${num}_face_300x300.png`
ava.width = "80"
yield ava
}
}
let getAvatar = avaGenerator ()
:mortar_board: yield*
:coffee: 14
const iterator1 = function* ( arg ) {
while ( true ) {
yield arg++
}
}
const iterator2 = (
function* ( arg ) {
while ( true ) {
arg < 5 ? yield arg++ : yield* iterator1(50)
}
}
)(0)
document.body.onclick = function ( event ) {
let iter = iterator2.next()
console.log ( iter.value )
}
:coffee: 15
const generator1 = function* ( arg ) {
while ( true ) {
arg++ < 5 ? yield "generator1: " + arg : yield* generator2()
}
}
function* generator2() {
while ( true ) {
Math.random() > 0.3 ? yield "generator2" : yield* generator1(0)
}
}
const iterator2 = generator2 (3)
document.body.onclick = function ( event ) {
let iter = iterator2.next()
console.log ( iter.value )
}
:coffee: 16
const generator = function* () {
yield* [ 5, 4, 3, 2, 1 ]
yield* "API"
yield* arguments
}
const iterator = generator ( 10, 20, 30 )
document.body.onclick = function ( event ) {
console.log ( iterator.next().value )
}
:mortar_board: return()
:coffee: 17
const generator1 = function* () {
while ( true ) {
let x = Math.round ( Math.random() * 10 )
x > 5 ? yield "generator1: " + x : yield* generator2()
}
}
function* generator2() {
while ( true ) {
Math.random() > 0.3 ? yield "generator2" : yield* generator1()
}
}
const iterator = generator2 (3)
document.body.onclick = function ( event ) {
let iter = iterator.next()
console.log ( iter.value )
if ( iter.value === "generator1: 8" ) iterator.return()
}
Передача параметров
Поскольку генератор имеет внутренний "тормоз" - yield,
и возобновление его работы с точки останова осуществляется внешним "пинком" - методом next() объекта-итератора,
логично, что именно в этой точке появляется возможность передать какие-то данные функции-генератору
Для этого нужно, чтобы в точке останова генератора было присваивание:
...
let param = yield ...
...
Тогда аргумент, переданный при вызове next(), попадет в переменную param
:coffee: 18
iterator = ( function* ( arg ) {
let ind = 0,
ret = arg,
d = new Date().getTime(),
key;
while ( true ) {
key = d === new Date().getTime() ? `${d}[${ind++}]` : new Date().getTime();
d = new Date().getTime();
ret = yield { [ key ]: ret };
}
})( "Hello" )
console.log ( iterator.next().value )
console.log ( iterator.next( "Welcome" ).value )
console.log ( iterator.next( "Who are you?" ).value )
console.log ( iterator.next( "Bye-bye..." ).value )
Symbol.asyncIterator
Если у объекта есть свойство [ Symbol.asyncIterator ],
и это асинхронная функция,
то этот объект можно итерировать циклом for await...of
:warning: Однако в обычном смысле этот объект не является итерабельным,
т.е. обычным оператором for...of его итерировать нельзя,
и к нему нельзя применить оператор spread
:coffee: 19
const promise = val => new Promise (
resolve => setTimeout (
() => resolve ( val ),
1000
)
)
const browsers = {
[ Symbol.asyncIterator ]: async function* () {
yield promise ( "Chrome" )
yield promise ( "Mozilla" )
yield promise ( "Safari" )
yield promise ( "IE" )
},
async show () {
for await ( let browser of browsers )
console.log ( browser )
}
}
browsers.show()
Но мы можем добавить объекту оба свойства - и [ Symbol.asyncIterator ], и [ Symbol.iterator ]
:coffee: 20
const promise = val => new Promise (
resolve => setTimeout (
() => resolve ( val ),
1000
)
)
const browsers = {
[ Symbol.asyncIterator ] : async function* () {
yield promise ( "Chrome" )
yield promise ( "FireFox" )
yield promise ( "Safari" )
yield promise ( "IE" )
},
[ Symbol.iterator ] : function* () {
yield "Google",
yield "Mozilla",
yield "Mac",
yield "Microsoft"
},
async show () {
for await ( let browser of browsers )
console.log ( browser )
}
}
browsers.show()
console.log ( ...browsers )
Array.from ( browsers ).forEach ( browser => console.log ( browser ) )