初めに
今回は[Symbol.asyncIterator]
、非同期ジェネレータについてまとめていきたいと思います。
参考文章はこちらです。
Async iteration and generators - javascript.info
Symbol.asyncIterator - MDN
[Symbol.asyncIterator]
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() {
return {
current: this.from,
last: this.to,
async next() {
await new Promise(resolve => setTimeout(resolve, 1000));
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// IIFE
(async () => {
for await (let value of range) {
console.log(value)
};
})()
参考文章からの例です。最近の勉強では初見の書き方が出てきたり、既定の書き方で書かねば動かないというのがよくあるので、下調べしてから自分なりに解釈してメモします。
今回の[Symbol.asyncIterator]
の非同期反復処理も既定の書き方で、
- 中身は
next()
メソッドを内蔵するオブジェクトを返す。 -
next()
ではPromise
オブジェクトを返す。あるいはasync next()
で返すオブジェクトをPromise
に保証する。
自分のテストでは二番目がnext()
からasync
とawait
を除いてもちゃんと動けますが、しかし非同期という目的が実現できません。ここはawait
でnext()
毎回の反復処理がnew Promise(resolve => setTimeout(resolve, 1000))
が終わるまで待ってから、下のコードを進むのです。
Using javascript's Symbol.asyncIterator with for await of loop - stackoverflowを参考して、next()
を使わないように書いてみました。
// generator is iterable object
async function* counter() {
for (let i = this.from; i <= this.to; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i
}
};
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]: counter
};
async function test() {
for await (let value of range) {
console.log(value)
}
};
test()
-
[Symbol.asyncIterator]
には反復可能のオブジェクト。([Symbol.asyncIterator](){}
の書き方ならオブジェクト返す関数にする。) - 非同期の実現は
async generator
でawait
とsetTimeout()
などの遅延処理メソッドと併用し、そしてasync function
でfor await (let item of iterable)
で結果をコンソール。
[Symbol.asyncIterator]
にはオブジェクト、async generator
にはyield
で値の転送、async function
にfor await (let item of iterable)
でジェネレータからのプロミスオブジェクトを待つ。
...いろいろと書き直してみたいけど、書き方が限られてる気がします。
下は参考文章からの書き方です。
let range = {
from: 1,
to: 5,
async *[Symbol.asyncIterator]() {
for (let value = this.from; value <= this.to; value++) {
await new Promise(resolve => setTimeout(resolve, 1000))
yield value
}
}
};
(async () => {
for await (let value of range) {
console.log(value)
}
})()
async *[Symbol.asyncIterator](){}
でfor
ループを包んで、await
で遅延処理を待ち、yield
で値を返す。下のIIFE
でプロミスオブジェクト返すのを待って値をコンソール。
こちらのほうが読みやすい書きやすくと思います。
real-life example: paginated data
これもまた参考文章からの例です。
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`
while (url) {
const response = await fetch(url, {
headers: { 'User-Agent': 'Our script' }
})
const body = await response.json()
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/)
nextPage = nextPage?.[1]
url = nextPage
for (let commit of body) {
yield commit
}
}
};
(async () => {
let count = 0
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
// console.log(commit.author)
console.log(commit.author.login)
if (++count == 10) {
break
}
}
})()
全体のプロセスとしては、
url
確定
↓
fetch()
から返されたレスポンスを待つ
↓
レスポンスをJSONにパースしたり、
ページ作りのためレスポンスからHeaders.get()
を使い、キーでLinkオブジェクトを選択し、中に/<(.*?)>; rel="next"/
に一致した値を取り出しnextPage
へ。
nextPage
が存在したら(null/undefined
でなければ)、Cache.match()
で返されたPromise
(ここは配列)インデックス[1]のリンクを取りurl
に書き換えて、全部が終わったら次のwhile
ループへ。
↓
レスポンスの全体からオブジェクト単位yield
経由で値を転送し、キーで特定範囲の文字列を取り出す。(ここはコンソール、ほかにHTMLに戻すとかも。)
1ページの表示上限を設定する。
これまでの感想やまとめ
APIドキュメントを参考しながら、要求に応じたresponseをJSONにパースしJSONから一定の規律を見つけて、処理の仕方やHTMLへどう表現していくを考えるとか、一定の順序で進めば難しくないですが。
以前もrequire()
でレスポンスからheaderやstateCodeなど取得して、それに対応するコードを実践したことがあるけれど、最近勉強メモの文章を書きながらやはり自分がHTTPリクエストへの認識が浅はかだなあと思います。
例えばrequestの処理の違いと、headerに注意すべきところ('User-Agent'が偽造できる問題とか)、CORSを利用してCSRF/XSS防止するなど、今の自分は基礎どころか入門レベルと思います。
より安全なWeb Appを作るためにこれからJavaScript以外、ネットワーク特にHTTP関連についてまとめていきたいと思います。