自分で非同期処理を行う関数を作成したい場合は、どのようにすればいいのでしょうか?
自分が考える「非同期処理を行う関数」の本質的なイメージは
(A) ある仕事をするための関数がその仕事が終わらないうちに呼び出し元へ戻ってくるもの
です。呼び出し元で仕事が終わったときに特定のことをしたいことも多いので上記に加え
(B) その仕事が終わったら指定したコールバック関数を自動的に呼び出してくれるもの
という条件を加えてもよいでしょう。
(1) 普通のケース(I/Oバウンドな処理の非同期関数)
世の中のJavaScriptのプログラマーは普段から自然に非同期な関数を書いていると思います。どうやって書いているかといえば単に「より低水準な非同期関数を利用しているだけ」といえましょう。ブラウザーにせよNodeJSにせよシングルスレッドによる非同期処理が基本戦略ですので非同期関数は豊富に用意されています。非同期関数を定義するというのは「JavaScriptで何かを書くこと」とほとんど同義であるといっても過言ではないと思います。
非同期な関数を利用して非同期な関数を定義する素朴な例を挙げますと
countdown.js
js
1function countDown(seconds, label, cb) {
2 var count = 0
3 const tid = setInterval(eachSecond, 1000);
4
5 function eachSecond() {
6 count += 1
7 console.log(`${label}: count is ${count}`)
8 if (count >= seconds) {
9 clearInterval(tid)
10 cb(label)
11 }
12 }
13}
14
15const cb = x => console.log(`${x} expired`)
16countDown(3, "A", cb)
17countDown(3, "B", cb)
bash
1$ node countdown.js
2A: count is 1
3B: count is 1
4A: count is 2
5B: count is 2
6A: count is 3
7A expired
8B: count is 3
9B expired
10$
setIntervalを利用したcountDown関数もまた非同期関数になりcountDownを2回コールするとそれらが並行して動作していることがわかります。
requestも本質的にはこういう類のものと思います。ソケットへの非同期I/Oをしてくれる低水準な非同期関数を用いてより高水準な非同期関数を定義しているイメージです。
(2) CPUバウンドな非同期関数
あまり一般的とは言えない例だと思いますが、(1)のようなI/OではなくJavaScriptそのもので大量の計算処理を行うような機能を非同期にしたい場合、シングルスレッドで行う方法と別のスレッド(または別のプロセス)を用いる方法が考えられると思います。
JavaScriptの主たる計算モデルであるシングルスレッドで行うなら「計算全体を細切れにして一時にほんの少しだけ計算を進める」という戦略を用い例えば次のようなすると非同期関数となります。
js
1const EventEmitter = require('events').EventEmitter
2let clockTick = 0
3
4function asyncRun(g, interval) {
5 const ee = new EventEmitter()
6
7 ee.on('next', () => {
8 if (!g.next().done) {
9 step()
10 }
11 })
12
13 function step() {
14 setTimeout(() => ee.emit('next'), interval)
15 }
16
17 step()
18}
19
20function* foo(n, cb) {
21 console.log('foo: invoked')
22 let s = 0
23 for (let i = 1; i <= n; i++) {
24 console.log(`foo: clockTick=${clockTick}: i=${i}`)
25 s += i
26 yield
27 }
28 cb(s)
29 console.log('foo: ends')
30}
31
32function asyncFoo(n, cb) {
33 asyncRun(foo(n, cb), 10)
34}
35
36asyncFoo(5, r => console.log(`callback: result=${r}`))
37
38const tid = setInterval(() => {
39 clockTick += 1
40 if (clockTick > 1000) {
41 clearInterval(tid)
42 console.log('interval timer stopped')
43 }
44}, 0)
45console.log('main: last line')
まず同期的にHTTPリクエストを発行し、結果を取り出す処理を少し後の実行キューに追加している
という質問者さんのイメージに近いのが上の例だと思いますが前述したようにこの方式は一般的とは言えないでしょう。いわゆる普通の(?)非同期関数はどちらかといえばI/Oを非同期に行うものという印象が強いので「少し後の実行キューに追加」ではなく「望む状態になったら(いつ完了するかわからないI/O動作が完了したら)自動的かつ即座に実行キューに追加される」とイメージすることが多いのではないでしょうか。
ちなみに別のスレッドでやるならworker_threads、別のプロセスでやるならprocessといったモジュールを利用すれば実現できますがやはり「あまり一般的な方法とは言えない」と思います。
(3) もっと低水準なもの
(1)(2)とは異なりJavaScriptから利用できる適切な関数が存在しないようなケースがあるかも知れません。そういう場合C/C++で実装した実現が可能です(少なくともNodeJSではそれが容易にできます)。細かくは言及しませんが、例えば「nodejs c++ 呼び出し」で検索するとNANというライブラリーがありNodeJSからC/C++で記述したプログラムと連携する方法があるということがわかります。
https://qiita.com/sassy_watson/items/25241868be12a425f86a
C/C++で実装するものですのでJavaScriptに望む機能がなくてもC/C++から利用できる多くの機能(例えばOSに対する直接的なシステムコールのようなもの)も利用でき「およそ計算機で実現可能なことならなんでもできる」といってよいと思います。
一番言いたいこと:
いくつかの例(1),(2),(3)を挙げましたが、おそらく質問者さん(あるいは多くのJavaScriptのアプリケーションプログラマー)にとって(2)や(3)のような方法が必要な場面はほぼないと思います。なぜなら「JavaScriptでやりたいような多くの機能」は既にだれかがJavaScriptの非同期関数として開発しているからです。音声・アニメーション・ファイルとのI/O・ネットワークを介した通信などおよそ必要と思われるほとんどの基本機能がブラウザーなりNodeJSで利用可能になっていますので普通のアプリケーションプログラマーは、主に(1)の方法でアプリケーションを(非同期関数の集まりとして)書いているだろうと思います。
要するに「非同期関数をどうやって定義するか」は「既に存在している非同期関数をどうやって使うか」つまり「JavaScriptでアプリケーションをどうやって書くか」と同義と思います。自分が一番いいたかったのはそういうことです。