以下の内容はhttps://let.blog.jp/tag/Promiseより取得しました。


Promise をまとめる系関数の動きを確認できる画面
https://nexpr.gitlab.io/public-pages/promise-merge/

Promise.all とか Promise.any とか Promise をまとめる関数があります
よく使う all はいいのですが たまに使うものだとどれだっけ?ってなることがあります
また誰かに伝えるときに説明ってしづらかったりするのですよね

ボタンを押して resolve/reject して結果が見れるのがあるとわかりやすいかなと思って確認できる画面を作ってみました

使い方は見たままですが……
一番上のボタンで all/any/race/allSettled を選びます
固定で 3 つの Promise が作られて それぞれの resolve/reject ボタンを押せます
押すとその Promise の状態が更新されます
3 つの Promise を指定の方法 (all や race) でまとめた結果の Promise の状態が一番下に出ます
Promise.all はそのまま引数に渡して使えない
これを実行するとエラーでした

const promises = [
[Promise.resolve(1), Promise.resolve(2)],
[Promise.resolve(3), Promise.resolve(4)],
]

promises.map(Promise.all)

Chrome では
「Promise.all called on non-object」

Firefox では
「Receiver of Promise.all call is not a non-null object」

Promise.all を関数として渡しているので内部的に呼び出されるときに Promise というオブジェクトのメソッドというコンテキストで呼び出されないからですが この制限があると

promises.map(x => Promise.all(x))

のように呼び出さないといけなくなって面倒なんですよね

昔は console.log も同じ問題があったのですが 結構前に解決されて

fetch("").then(x => x.text()).then(console.log)

みたいに then に直接渡すだけでも良くなりました

組み込み関数なら全てこういう風にしてほしいものです
this の考え方が関数渡しに適してないので this は消えてほしいです

自動で this を bind 済みにしてくれる記法ができればいいんですけどね

obj..method



obj.method.bind(obj)

と同じになるみたいな
export したいデータの取得に非同期処理が入る時
非同期関数を export してそれを使う側が呼び出すのがよくあるやつ

[index.html]
<script type="module">
import getData from "./module1.js"

!async function() {
const data = await getData()
document.body.append(data.text)
}()
</script>

[module1.js]
export default () => {
return new Promise(r => {
setTimeout(() => {
r({ text: "loaded" })
}, 3000)
})
}

だけど考えてみると Promise をエクスポートすることもできるのでこれで良さそう

[index.html]
<script type="module">
import data from "./module1.js"

!async function() {
const { text } = await data
document.body.append(text)
}()

</script>

[module1.js]
export default new Promise(r => {
setTimeout(() => {
r({ text: "loaded" })
}, 3000)
})

呼び出すたびに新しく取得する必要のないデータなら 初回呼び出しの結果をキャッシュして 2 回目以降を非同期処理なしで前回の結果を返す仕組みを自作しなくてもいいし
make-promises-safe
make-promises-safe というパッケージを使うことを推奨してるものがあったので どんなのだろうって見てみたら たった 15 行のコード
中身は Node.js で unhandledRejection が起きたときに強制終了させるというもの
Promise が reject されたのにそれを catch してないのは実装ミスでメモリリークにつながるからそれを防ぐためのものらしい
使い方はトップレベルで一回インポートするだけ
それで process.on を使ってリスナ登録される

設定可能な部分は
◯ 強制終了の手段に process.exit か process.abort かを選べる
◯ rejection の reason をログ出力するのに使う関数を設定できる (デフォは console.error)

くらい
コード見た感じだと非同期のログ書き込みだと process.exit のほうが先に行われて書き込みできなそう

あえてライブラリを npm から入れなくても自分でこう書いとけば十分

process.on("unhandledRejection", reason => {
console.error(reason)
process.exit(1)
})

これでリポジトリにスターが 400 近くあるのはちょっと驚き
async 関数で promise を返す
promise を promise で包むと勝手に展開される
なので async 関数が promise 返しても then で受け取れるのは promise じゃなくて promise の中の値

async function foo() {
const value = await something()
const promise = longProcess(value.bar)
return promise
}

async function main() {
const promise = await foo()
promise.then(() => {
console.log("LONG PROCESS END")
})
console.log("LONG PROCESS START")
}

main()

await foo() の結果の promise は longProcess が返す promise の中の値なので then はなくてエラー
オブジェクトに包めば展開されないので promise を返せる

async function foo() {
const value = await something()
const promise = longProcess(value.bar)
return { promise }
}

async function main() {
const { promise } = await foo()
promise.then(() => {
console.log("LONG PROCESS END")
})
console.log("LONG PROCESS START")
}

main()

⇩実行用に longProcess とかのサンプル実装

const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
const something = async () => {
await wait(1000)
return { foo: 1, bar: 2 }
}
const longProcess = async () => {
await wait(5000)
}
EventEmitter を Promise や Generator でする
◯ EventEmitter の基本的な使い方

毎秒時刻をコンソールに表示

const emitter = new EventEmitter()
emitter.on("aaa", value => console.log(value))
setInterval(() => emitter.emit("aaa", Date.now()), 2000)

Promise も非同期処理の通知ができるけど一回限り
二回目以降の通知はできず 終了済みの値は保持されて終了してれば待機せず即実行できる

◯ Promise で EventEmitter みたいなことをやってみる

Promise の結果に本来の結果と次の Promise を入れて 結果を受け取ったら次は新しく受け取った Promise の resolve を待機する

const notifyPromise = () => {
let resolve
const promise = new Promise(a => (resolve = a))
const notify = value => {
const r = resolve
const p = new Promise(a => (resolve = a))
r([value, p])
}
return [notify, promise]
}

const [notify, promise] = notifyPromise()
Promise.resolve().then(async () => {
let p = promise
while (true) {
const [value, next] = await p
try {
console.log(value)
} finally {
p = next
}
}
})
setInterval(() => notify(Date.now()), 2000)

await の無限ループにするので then の中でやらないとメインの処理が進まなくなる
Promise の入れ替えなどちょっと面倒

◯ for await of のループを利用する

generator にして Promise を受け取る
for-of を使って Promise の入れ替えを考えなくていいようにする

const f = () => {
let resolve
const wait = () => new Promise(r => (resolve = r))
async function* g() {
while (true) yield await wait()
}
const notify = v => resolve(v)
const it = g()
return [notify, it]
}

const [notify, it] = f()
Promise.resolve().then(async () => {
for await (const value of it) {
console.log(value)
}
})
setInterval(() => notify(Date.now()), 2000)

generator の中で作った関数を公開して その関数の中で yield できたら楽だったけど generator スコープ中でしか yield できない
なので emit に当たる関数が呼び出されるまで待機するには await での待機が必要
async generator にして await で待機してから yield する
エラー処理の方法
async function case1() {
// ...

const result = await something().catch(err => err)

if (result instanceof Error) {
console.error(result)
if (result.display_message) notifyUser(result.display_message)
return
}

// ...

return result.data
}

async function case2() {
// ...

let result
try {
result = await something()
} catch (err) {
console.error(err)
if (err.display_message) notifyUser(err.display_message)
return
}

// ...

return result.data
}

case2 は見づらいので case1 のほうが好き
速度の違いを調べてみる

async function something(n) {
if (n & 1) throw new Error("error")
return { data: "ok" }
}

async function case1(n) {
const result = await something(n).catch(err => err)

if (result instanceof Error) {
if (result.display_message) notifyUser(result.display_message)
return { ok: false, error: result }
}

return { ok: true, value: result.data }
}

async function case2(n) {
let result
try {
result = await something(n)
} catch (err) {
if (err.display_message) notifyUser(err.display_message)
return { ok: false, error: err }
}

return { ok: true, value: result.data }
}

async function measure1() {
console.time(1)
for (let i = 0; i < 10000; i++) {
await case1(i)
}
console.timeEnd(1)
}

async function measure2() {
console.time(2)
for (let i = 0; i < 10000; i++) {
await case2(i)
}
console.timeEnd(2)
}

!async function () {
await measure1()
await measure2()
await measure1()
await measure2()
await measure1()
await measure2()
await measure1()
await measure2()
}()
1: 258.104736328125ms
2: 198.275146484375ms
1: 252.72900390625ms
2: 210.5810546875ms
1: 286.694091796875ms
2: 210.133056640625ms
1: 262.489990234375ms
2: 220.68994140625ms

2 のほうが微妙に速かった
と言っても倍にもなってないし 1 万回で 40ms 程度の差なので気にせず 1 を使うつもり

ただ今回みたいなケースだと これで十分そう

async function case3(n) {
return something(n).then(
result => ({ ok: true, value: result.data }),
err => {
if (err.display_message) notifyUser(result.display_message)
return ({ ok: false, error: err })
}
)
}

成功時の処理が長いならネストが深くなって見づらくなるので case1 の方法のほうが良さそう
case3 の速度は case1 と case2 の間くらい

自分で catch 書きたくなくて ステータスとして成功・失敗を結果に含んでくれればいいのにと思ってたら最近できた Promise.allSettled がいい感じ

async function case4(n) {
const [result] = await Promise.allSettled([something(n)])

if (result.status === "rejected") {
if (result.reason.display_message) notifyUser(result.reason.display_message)
return { ok: false, error: result.reason }
}

return { ok: true, value: result.value.data }
}

ただ 本来は複数の Promise 全部が成功か失敗で終了したときに resolve される Promise を取得する用途なので配列になる
それでも見た目的にはよさそう
肝心の速度は case1 よりもちょっと遅めだった
非同期処理を順番にする書き方
// 1
async function case1() {
try{
await asyncAction1()
await asyncAction2()
await asyncAction3()
} catch (err) {
console.error(err)
}
}

// 2
function case2() {
return chain([
asyncAction1,
asyncAction2,
asyncAction3,
]).catch(console.error)
}

function chain(afns) {
return afns.reduce((a, b) => a.then(b), Promise.resolve())
}

// 3
function case3() {
return Promise.resolve()
.then(asyncAction1)
.then(asyncAction2)
.then(asyncAction3)
.catch(console.error)
}

1 は async/await な方法
各処理の間に何かやりたいときにやりやすい
動的に実行関数変えるなら配列にして for-of で回すだけ

2 は Promise をチェーンさせる関数を使ったもの
関数作る必要あるけど chain は汎用的なものだし 1 回つくれば使い回せるからそこまで気にならない
関数の配列渡すだけなのでシンプル

3 は自分でチェーンさせたもの
2 よりこれのほうが見やすいかもだけど 動的に項目変わると対応しづらい
配列だけ作って渡せばいい 2 のほうが扱いやすい
bluebird って ES2015 Promise より速かったんだ
ES2015 になってから結構経つのにまだ bluebird を使った Promise をわりと見かけます
古いものはともかく コードは class 構文をつかうなど ES2015 構文で書かれているにもかかわらずです
標準に比べるとキャンセルとか機能は少し多めですが 標準で組み込まれてる機能があるならそっちのほうがパフォーマンスは優秀だと思いますし Promise は特に内部的に違いがあって JavaScript で完全に再現できないものもあります
Promise.resolve().then() の非同期処理は timeout とはまた少し扱いが違うなどです
標準なものにも 現在の状態 (すでに resolve されているか) がわからないなど不便なところは無いとは言えませんが Promise の仕組み自体を JavaScript で作るよりは標準のものを使ってそれをラップして機能追加するほうが良いように思います

しかし 調べてみたらちょっと意外でした
標準 Promise より bluebird のほうが 高速らしいです
https://softwareengineering.stackexchange.com/questions/278778/why-are-native-es6-promises-slower-and-more-memory-intensive-than-bluebird
bluebird の Promise が ES2015 の仕様に完全準拠してるわけじゃないというのが理由みたいです

ただ それも Node.js 10 でネイティブの Promise が高速化したので bluebird のほうが速いとは限らないようです
もう 10 の LTS が開始しましたし いまから bluebird にしようかと考える必要はないかもしれません
Promise の回収
Node.js でこれを実行したら 1 秒後に 1 を表示して Node.js のプロセス自体が終了する

function a(){
return new Promise(resolve => {
setTimeout(() => {
console.log(1)
return 2
}, 1000)
})
}

function rec(){
a().then(e => rec())
}

rec()

ずっと繰り返されそうにと思ってたけど Promise 内で resolve を使わず return してた
こういう resolve も reject もせず放置された Promise ってちゃんと回収されるのかなんか不安があったけどちゃんと回収されてるみたいで安心

ところで return 2 を resolve(2) に直すと毎秒 1 がで続ける
2 には特に意味はない

今回のだと絶対に resolve も reject もされないってわかるからであって fetch したときにサーバが end 忘れててレスポンスが永遠に来ないとかだとダメだと思う
そういうことはあまりなさそうだけど WebSocket のライブラリで socket に対してレスポンスを返せるもので返し忘れはわりとありそうな気はする



以上の内容はhttps://let.blog.jp/tag/Promiseより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14