JavaScriptと非同期プログラミング
8061 ワード
何が非同期ですか?
ウィキペディアによると、主なコントロールフローから独立して発生した事件を非同期といいます.例えば、ある順序で実行されるコードがあります.
JavaScript-生まれつきの非同期の言語
しかし、非同期プログラムの真の発展は壮大で、Ajaxの流行は不可欠です.Ajaxの中のA(Aynchronous)は本当に非同期の概念に達しました.これはまだIE 5、IE 6の時代です.
コールバック関数——非同期プログラミングの痛み
非同期タスクを実行した後、開発者にどうやって通知しますか?コールバック関数は最もシンプルで、考えやすい実現方法です.そこで、非同期プログラムが生まれた時から、それはコールバック関数と一緒に縛られました.
例えばsetTimeout.この関数は指定された時間を超えて指定された関数を実行するタイマーを開きます.例えば、一秒後に数字1を出力します.コードは以下の通りです.
解法も簡単です.前のタイマーがタイムアウトしたらもう一つのタイマーを起動してください.コードは以下の通りです.
コールバック関数が存在するため、ループは使えません.循環できないなら、再帰を考慮するしかないです.
ジェネナート——JavaScriptの中の半協程
多くの言語が協働プログラムを導入して、非同期プログラムを簡略化します.JavaScriptにも似たような概念があります.Generatorといいます.
MDN上の説明:Generatorは途中で退出した後に再び入ることができる関数です.彼らの関数の文脈は再入力するたびに維持されます.簡単に言えば、
簡単な例を挙げます.は、 は は を出力します.呼び出し は を出力します.呼び出し は です.
Generator関数を呼び出したら、1つのサブジェネレータに戻ります.ユーザーが自動的に
例えば、
Generatorは非同期ですか?
Generatorも半協程と言っています.自然と非同期の関係は浅からずです.Generatorは非同期ですか?
でもないです.前に述べたように、非同期は相対的で、例えば上記の例である.
Generatorで非同期コードを簡略化する
最初の問題に戻ります.は、 は サイクルが開始され、iは1に初期化される. は は 1秒後、 呼び出し 回のサイクルが終了し、iは2に増加し、第4ステップに戻って を実行し続ける.… これにより同期sleepのような要求が実現される.
async、await――同期文法で非同期コードを書きます.
上のコードは結局手動でディエゼル変数を定義する必要があります.また、ハンドメイド
私たちは
注意時には、いくつかの非同期イベントが並行して実行される必要があります.(例えば、2つのインターフェースを呼び出して、2つのインターフェースが戻ったら後続コードを実行します.)
結尾語
今は
nodeでServerを書くなら、気にしないで使ってもいいです.koaは
終了
ウィキペディアによると、主なコントロールフローから独立して発生した事件を非同期といいます.例えば、ある順序で実行されるコードがあります.
void function main() {
fA();
fB();
}();
fA=>fBは順番に実行されます.いつまでもfAはfBの前で実行されます.彼らは同期の関係です.加入時にsetTimeout
を使用してfAを遅延させる.void function main() {
setTimeout(fA, 1000);
fB();
}();
このとき、fAはfBに対して非同期である.main関数は、一秒後に一回fAを実行すると宣言しただけで、すぐに実行しませんでした.このとき、fAの制御フローはメインから独立している.JavaScript-生まれつきの非同期の言語
setTimeout
の存在のため、少なくともECMAによって標準化された時点から、JavaScriptは非同期プログラミングをサポートしている.他の言語のsleep
と違って、setTimeout
は非同期であり、現在のプログラムの継続を妨げることはない.しかし、非同期プログラムの真の発展は壮大で、Ajaxの流行は不可欠です.Ajaxの中のA(Aynchronous)は本当に非同期の概念に達しました.これはまだIE 5、IE 6の時代です.
コールバック関数——非同期プログラミングの痛み
非同期タスクを実行した後、開発者にどうやって通知しますか?コールバック関数は最もシンプルで、考えやすい実現方法です.そこで、非同期プログラムが生まれた時から、それはコールバック関数と一緒に縛られました.
例えばsetTimeout.この関数は指定された時間を超えて指定された関数を実行するタイマーを開きます.例えば、一秒後に数字1を出力します.コードは以下の通りです.
setTimeout(() => {
console.log(1);
}, 1000);
常規用法もし需要が変わったら、毎秒1つの数字を出力する必要があります.(もちろんsetIntervalではありません.)JavaScriptの初心者はこのようなコードを書くかもしれません.for (let i = 1; i < 10; ++i) {
setTimeout(() => { // !
console.log(i);
}, 1000);
}
実行結果は1秒待ちで、すべての結果を一度に出力しました.ここのサイクルは10のタイマーを同時に起動しているので、タイマーごとに1秒を待っています.もちろん、すべてのタイマーが1秒後にタイムアウトし、コールバック関数が起動されます.解法も簡単です.前のタイマーがタイムアウトしたらもう一つのタイマーを起動してください.コードは以下の通りです.
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
setTimeout(() => {
console.log(4);
setTimeout(() => {
console.log(5);
setTimeout(() => {
// ...
}, 1000);
}, 1000);
}, 1000)
}, 1000)
}, 1000)
}, 1000);
層の入れ子、結果はこのような漏斗型コードです.新しい基準のPromiseを思いつき、次のように書き換えられるかもしれません.function timeout(delay) {
return new Promise(resolve => {
setTimeout(resolve, delay);
});
}
timeout(1000).then(() => {
console.log(1);
return timeout(1000);
}).then(() => {
console.log(2);
return timeout(1000);
}).then(() => {
console.log(3);
return timeout(1000);
}).then(() => {
console.log(4);
return timeout(1000);
}).then(() => {
console.log(5);
return timeout(1000);
}).then(() => {
// ..
});
漏斗形コードはなくなりましたが、コード量自体はそれほど減っていません.Promise
は、コールバック関数をドライできませんでした.コールバック関数が存在するため、ループは使えません.循環できないなら、再帰を考慮するしかないです.
let i = 1;
function next() {
console.log(i);
if (++i < 10) {
setTimeout(next, 1000);
}
}
setTimeout(next, 1000);
再帰的な書き方であるが、next
関数はすべてブラウザで呼び出されるので、実際には再帰関数のコールスタック構造はない.ジェネナート——JavaScriptの中の半協程
多くの言語が協働プログラムを導入して、非同期プログラムを簡略化します.JavaScriptにも似たような概念があります.Generatorといいます.
MDN上の説明:Generatorは途中で退出した後に再び入ることができる関数です.彼らの関数の文脈は再入力するたびに維持されます.簡単に言えば、
Generator
と一般Function
との最大の違いは、Generator
自身が前回の呼び出しの状態を保持していることである.簡単な例を挙げます.
function *gen() {
yield 1;
yield 2;
return 3;
}
void function main() {
var iter = gen();
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
}();
コードの実行順序はこうです.gen
を要求し、1つのローズマリーiter
を得る.注意このときは、gen
の関数体が本当に実行されていません.iter.next()
を呼び出し、gen
の関数を実行する.yield 1
に出会い、1を返します.iter.next()
の戻り値は{done:false、value:1}で、1 iter.next()
.前回yield
が出たところから引き続きgen
が実行される.yield 2
に出会い、2を返します.iter.next()
の戻り値は{done:false、value:2}で、2 iter.next()
.前回yield
が出たところから引き続きgen
が実行される.return 3
に遭遇し、3を返し、return
は関数全体が実行済みであることを示している.iter.next()
の戻り値は{done:true,value:3}、出力3 Generator関数を呼び出したら、1つのサブジェネレータに戻ります.ユーザーが自動的に
iter.next()
を呼び出すと、このGenerator関数は本当に実行されます.例えば、
for ... of
を使って、iteratorを巡回してもいいです.for (var i of gen()) {
console.log(i);
}
出力1 2
、最後のreturn 3
の結果は含まれていません.Generator
の各項目で1つの配列を生成するのも簡単で、Array.from(gen())
または[...gen()]
を直接使用すればよく、[1, 2]
を生成するのも同様に最後のreturn 3
を含まない.Generatorは非同期ですか?
Generatorも半協程と言っています.自然と非同期の関係は浅からずです.Generatorは非同期ですか?
でもないです.前に述べたように、非同期は相対的で、例えば上記の例である.
function *gen() {
yield 1;
yield 2;
return 3;
}
void function main() {
var iter = gen();
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
}();
私たちは直感的に見ることができます.genのメソッドはmainのメソッドと交互に実行されていますので、genはmainに対して非同期的に実行されています.しかし、このプロセスでは、全体の制御フローはブラウザに渡されていないので、genとmainはブラウザに対して同期して実行されるということです.Generatorで非同期コードを簡略化する
最初の問題に戻ります.
for (let i = 0; i < 10; ++i) {
setTimeout(() => {
console.log(i);
}, 1000);
// setTimeout
}
キーは、前のsetTimeout
がフィードバックをトリガしてから次のループを実行する方法です.Generator
を使用すると、setTimeout
の後にyield
が外に出て(制御ストリームをブラウザに返す)、setTimeout
によってトリガされるコールバック関数next
を考慮して、制御ストリームをコードに戻して、次のループを実行します.let iter;
function* run() {
for (let i = 1; i < 10; ++i) {
setTimeout(() => iter.next(), 1000);
yield; // setTimeout
console.log(i);
}
}
iter = run();
iter.next();
コードの実行順序はこうです.run
を要求し、1つのローズマリーiter
を得る.注意このときは、run
の関数体が本当に実行されていません.iter.next()
を呼び出し、run
の関数を実行する.setTimeout
を実行し、タイマーを起動し、コールバック関数は1秒後に実行される.yield
に遭遇し、制御ストリームは最後のyield undefined
に戻る.後に他のコードがないので、ブラウザはコントロールを得て、ユーザイベントに応答して、他の非同期コードなどを実行します.iter.next()
がタイムアウトし、コールバック関数setTimeout
が実行される.() => iter.next()
.前回iter.next()
が出たところから引き続き、すなわちyield
が、iの値を出力する.async、await――同期文法で非同期コードを書きます.
上のコードは結局手動でディエゼル変数を定義する必要があります.また、ハンドメイド
console.log(i)
も必要です.もっと重要なのはnext
と連結されていて、通用しません.私たちは
setTimeout
が非同期プログラミングの未来であることを知っています.Promise
とPromise
を組み合わせて使ってもいいですか?このように考えた結果がasync関数です.Generator
でコードを取得すると以下の通りです.function timeout(delay) {
return new Promise(resolve => {
setTimeout(resolve, delay);
});
}
async function run() {
for (let i = 1; i < 10; ++i) {
await timeout(1000);
console.log(i);
}
}
run();
Chromeの設計文書によれば、async
関数内部はasync
によって実行される.Generator
関数自体は、run
関数がいつ実行されたかを知るためにPromise
を返します.したがって、run
の後ろにrun()
があり、直接.then(xxx)
があります.注意時には、いくつかの非同期イベントが並行して実行される必要があります.(例えば、2つのインターフェースを呼び出して、2つのインターフェースが戻ったら後続コードを実行します.)
const a = await queryA(); // queryA
const b = await queryB(); // queryB
doSomething(a, b);
このときawait run()
およびawait
はシリアルで実行される.少し修正できます.const promiseA = queryA(); // queryA
const b = await queryB(); // queryB 。 queryA 。
const a = await promiseA(); // queryB 。 queryA
doSomething(a, b);
個人的には次のような書き方が好きです.const [ a, b ] = await Promise.all([ queryA(), queryB() ]);
doSomething(a, b);
queryA
とqueryB
を組み合わせて使うと、より効果的です.結尾語
今は
await
の関数が主要なブラウザによって実装されました.古いバージョンのブラウザに対応する場合、Promise
を使用してasync
にコンパイルすることができる.まだ互換性があるなら、ES 5のみをサポートするブラウザも、babel
をGenerator
にコンパイルし続けることができる.コンパイルしたコードの量が大きいので、コードの膨張に注意してください.nodeでServerを書くなら、気にしないで使ってもいいです.koaは
Generator
を使うのがあなたの良いヘルパーです.終了