Edited at
NextremerDay 17

イテレーターとジェネレーターと非同期処理

More than 1 year has passed since last update.

Nextremer Advent Calendar 2016の17日目の記事です。


1. イテレーター

まずはイテレーターから見ていきます。

JavaScriptにおいて、イテレーターの仕組みは


  • イテレーター (Iterator)

  • イテレーターリザルト (IteratorResult)

  • イテラブル (Iterable)

の3要素から成り立っています。


1-1. イテレーター (Iterator)

反復処理を実行するオブジェクト。


  • Iterator Interface の必須プロパティ

プロパティ

next
イテレーターリザルトを返す関数


イテレーターの例

// 無限にhelloを返すイテレーター

const iterator = {};
iterator.next = function() {
const iteratorResult = { done: false, value: "hello" }
return iteratorResult;
};


1-2. イテレーターリザルト (IteratorResult)

反復処理の結果を保持するオブジェクト。


  • IteratorResult Interface のプロパティ

プロパティ

done
反復処理が終点に達した場合はtrue、達していない場合はfalse

value
反復処理で返す値。値はどんな型でも良い


1-3. イテラブル (Iterable)

自身に対して反復処理が可能なオブジェクト。


  • Iterable Interface の必須プロパティ

プロパティ

[Symbol.iterator]
イテレーターを返す関数


イテラブルなオブジェクトの例1

/*

* 無限にhelloを返すイテレーターを持つイテラブルなオブジェクト
*/

const iterableObject = {};
iterableObject[Symbol.iterator] = function() {
const iterator = {};
const iteratorResult = { done: false, value: "hello" };
iterator.next = function() {
return iteratorResult;
}
return iterator;
};

const it = iterableObject[Symbol.iterator]();
console.log(it.next()); // { done: false, value: "hello" }
console.log(it.next()); // { done: false, value: "hello" }
console.log(it.next()); // { done: false, value: "hello" }

// for (value of iterable) 構文にかけると無限にhelloを返し続けるので注意



イテラブルなオブジェクトの例2

/*

* 1-10を返すイテレーターを持つイテラブルなオブジェクト
*/

const iterableObject = {};
iterableObject[Symbol.iterator] = function() {
const iterator = {};
let count = 0;
iterator.next = function() {
count++;
let iteratorResult = { done: false, value: count };
if (10 < count) {
iteratorResult = { done: true };
}
return iteratorResult;
}
return iterator;
};

let count;
for (count of iterableObject) {
console.log(count); // 1...10
}



2. ジェネレーター

ジェネレーターを用いると、イテラブルなオブジェクトを簡単に作成することができます。


2-1. ジェネレーター関数 (GeneratorFunction)



  • function* 宣言を使用して定義された関数

  • ジェネレーター関数を実行するとジェネレーターが生成される


  • yield または yield* を用いて、ジェネレーターで返却する値を記述する


  • yield* はイテラブルなオブジェクトのイテレーターを実行し、各イテレーターリザルトの valueyield する


ジェネレーター関数の例

function* fiveCounter() {

yield 1;
yield* [ 2, 3, 4, 5 ]; // for (value of iterable) yield value; と等価
}


2-2. ジェネレーター (Generator)


  • イテレーターでありイテラブルなオブジェクト

  • ジェネレーター関数より生成される


  • next() を呼び出すことで、ジェネレーター関数で定義した処理を実行する


ジェネレーターの例

// ジェネレーター関数

function* fiveCounter() {
yield 1;
yield* [ 2, 3, 4, 5 ];
}

// ジェネレーターを生成
const generator = fiveCounter();

// ジェネレーターはイテラブルなオブジェクト
console.log(Symbol.iterator in generator); // true

// ジェネレーターはイテレーターでもある
console.log(generator === generator[Symbol.iterator]()); // true

// イテレーターなのでnext()で反復処理を行う
// 戻り値はイテレーターリザルト
console.log(generator.next()); // { done: false, value: 1 }
console.log(generator.next()); // { done: false, value: 2 }
console.log(generator.next()); // { done: false, value: 3 }
console.log(generator.next()); // { done: false, value: 4 }
console.log(generator.next()); // { done: false, value: 5 }
console.log(generator.next()); // { done: true, value: undefined }



3. ジェネレーターを使って非同期処理を同期的に書く

ジェネレーターにはイテラブルなオブジェクトを簡単に作成できるという特徴の他に、関数の実行を一時停止させることができるという特徴があります。


関数の実行を一時停止させる例

function* generatorFunction() {

console.log("step1");
yield;
console.log("step2");
yield;
console.log("step3");
}
const generator = generatorFunction();

// next()を実行するまで、yieldで停止し、次のconsole.log()は実行されない
generator.next(); // "step1"
generator.next(); // "step2"
generator.next(); // "step3"


また、generator.next(value)とすることでジェネレーターに値を渡すことができます。


ジェネレーターに値を渡す例

function* generatorFunction() {

const a = yield "step1";
const b = yield "step2";
yield a + b;
}
const generator = generatorFunction();

console.log(generator.next()); // { done: false, value: "step1" }

// yield "step1" が 1 に置き換わる
console.log(generator.next(1)); // { done: false, value: "step2" }

// yield "step2" が 2 に置き換わる
console.log(generator.next(2)); // { done: false, value: 3 }

console.log(generator.next()); // { done: true, value: undefined }


つまり、yield で非同期処理を待って、generator.next(value)で結果をジェネレーターに渡すようにすれば、ジェネレーター関数内で非同期処理を同期的に書くことができます。


非同期処理を同期的に書く例

function run(generatorFunction) {

const generator = generatorFunction();
function next(data) {
const iteratorResult = generator.next(data);
if (!iteratorResult.done) {
const res = iteratorResult.value(function(data){
next(data);
});
if (res && res.constructor === Promise) {
res.then(function(data) {
next(data);
});
}
}
}
next();
}

function asyncFunction(next) {
setTimeout(function() {
next(new Date());
}, 1000);
}

function promiseFunction() {
return new Promise(function(resolve) {
resolve(new Date());
});
}

run(function* () {
console.log(yield promiseFunction);
console.log(yield asyncFunction);
console.log(yield promiseFunction);
console.log(yield asyncFunction);
});