Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8909

[Node.js] 非同期処理 - Promise編

$
0
0

ES2015で導入された非同期処理の状態と結果を表現するオブジェクトです。
Promiseを利用した非同期関数の実装では、関数は呼び出されるとその場ですぐPromiseインスタンスを返す必要がありますが、処理の結果を確定するのはあとから(非同期)です。結果が未確定のPromiseインスタンスの状態をpendingといい、非同期処理に成功した状態をfulfilledと呼ぶ。一度でも"fulfilled"または"rejected"になったPromiseインスタンスの状態はそれ以降変化せず、これらの状態をsettledと総称します。

functionparseJSONAsync(json){returnnewPromise((resolve,reject)=>{setTimeout(()=>{try{resolve(JSON.parse(json))}catch(err){reject(err)}},1000)})}

Promiseのコンストラクタを使ってPromiseインスタンスを作っています。Promiseのコンストラクタは関数をぱらめーたとし、この関数はresolve, rejectという2つの関数を引数として実行されます。コンストラクタが実行された時点ではこのPromiseインスタンスの状態はpendingです。処理結果を引数にresolve()を実行するとfulfilledになり、エラーを引数にreject()を実行すると、"rejected"になります。
※ resuleve()の引数は必須ではなく、引数なしで実行するとからの値を持ったPromiseになる
※ reject()の引数も同様に非必須で、かつ任意の値を利用できるが、通常はErrorのインスタンスを引数に渡す

consttoBeFulfilled=parseJSONAsync('{"foo": 1}');consttoBeRejected=parseJSONAsync('不正なJSON');console.log('****** Promise生成直後 ******');console.log(toBeFulfilled);console.log(toBeRejected);setTimeout(()=>{console.log('******* 1秒後 *******');console.log(toBeFulfilled);console.log(toBeRejected);},1000)>>>******Promise生成直後******Promise{<pending>}Promise{<pending>}(node:9042)UnhandledPromiseRejectionWarning:SyntaxError:UnexpectedtokeninJSONatposition0atJSON.parse(<anonymous>)...(省略)*******1秒後*******Promise{{foo:1}}Promise{<rejected>SyntaxError:UnexpectedtokeninJSONatposition0atJSON.parse(<anonymous>)...(省略)}

生成した2つのPromiseインスタンスの状態は最初どちらもpendingです。1秒経過すると正常なJSONを渡した方はそのパース結果を保持したfulfilled状態となり、不正なJSONを渡した方はrejected状態になります。

Promiseインスタンスの状態以外に注目すべき点は、UnhandledPromiseRejectionWarningが出力されています。これはrejected状態になったPromiseインスタンスに対して、イベントループが次のフェーズに進むまでエラーハンドリングが行われなかった場合に出力される警告です。この時、processオブジェクトがUnhandledRejectionイベントを発行します。UncaughtExceptionと異なり、このイベントはREPLでも発行され、デフォルトではこのイベントが発行される状況でもNode.jsのプロセスが落ちることはありません。

process.on('UnhandledRejection',(err,promise)=>console.log('UnhandledRejection発生',err));

UnhandledPromiseRejectionWarningで述べられているとおり、UnhandledRejectionが発行されている状況でNode.jsのプロセスを終了させたい場合は、nodeコマンド実行時に、--unhandled-rejections=strictを指定します。
そのほかにprocess.exit(1);などを使ってプロセスをエラー終了する方法もあります。

pendingを経ず、fulfilledまたはrejectedなPromiseインスタンスを直接生成したい場合、Promiseのコンストラクタも使えるが、より簡単な手段として、Promise.resolve(), Promise.reject()が用意されています。

constresult1=newPromise(resolve=>resolve({foo:1}))constresult2=Promise.resolve({foo:1})constresult3=newPromise((resolve,reject)=>reject(newError('エラー')))constresult4=Promise.reject(newError('エラー'))console.log(result1)console.log(result2)console.log(result3)console.log(result4)>>>Promise{{foo:1}}Promise{{foo:1}}Promise{<rejected>Error:エラー...(省略)}Promise{<rejected>Error:エラー...(省略)}

then(), catch(), finally()

then()

then()はPromiseインスタンスの状態がfulfilledまたはrejectedになった時実行するコールバックを登録するメソッドです。

promise.then(value=>{// 成功時の処理},err=>{// 失敗時の処理})

onFulfilledの引数には解決済みのPromiseインスタンスの値が、onRejectedの引数には拒否理由が渡されます。then()の戻り値は、登録したコールバックの戻り値で解決される新しいPromiseインスタンスです。また、then()の実行は元のPromiseインスタンスには影響を及ぼしません。
元のPromiseインスタンスが何らかの理由で拒否された時、onRejectedを省略するとthen()の戻り値のPromiseインスタンスも同じ理由で拒否されます。

conststringPromise=Promise.resolve('{"foo": 1}');console.log(stringPromise);constnumberPromise=stringPromise.then(str=>str.length);constunrecoveredPromise=Promise.reject(newError('エラー')).then(()=>1)setTimeout(()=>{console.log(numberPromise);console.log(unrecoveredPromise)},1000)>>>Promise{'{"foo": 1}'}(node:9334)UnhandledPromiseRejectionWarning:Error:エラー...(省略)Promise{10}Promise{<rejected>Error:エラー...(省略)

一方、onRejectedを省略せずに何か値を返すようにするとその値で解決されたPromiseインスタンスが得られます。この結果UnhandledPromiseRejectionWarningが出力されなくなります。

constunrecoveredPromise=Promise.reject(newError('エラー')).then(()=>1,err=>err.message)setTimeout(()=>console.log(unrecoveredPromise),1000)>>>Promise{'エラー'}

then()によるPromiseのチェーンで非同期処理の逐次実行を容易に実装できます。

asyncFunc1(input).then(asyncFunc2).then(asyncFunc3).catch(err=>{// エラーハンドリング})

catch()

catch()も非同期処理の結果をハンドリングするためのインスタンスメソッドの1つです、then()の引数に渡すonFulfilled, onRejectedはどちらも省略可能で、then()の代わりにcatch()を使用できます。

constcatchedPromise=Promise.reject(newError('エラー')).catch(()=>0)setTimeout(()=>console.log(catchedPromise),1000)>>>Promise{0}

Promiseチェーンの最後にcatch()を記述すれば、エラーハンドリングを1箇所に集約できます。

asyncFunc1(input).then(asyncFunc2,err=>{// エラーハンドリング}).then(result=>{// 処理},err=>{// asyncFunc2用のエラーハンドリング})asyncFunc1(input).then(asyncFunc2).then(result=>{// 処理}).catch(err=>{// エラーハンドリングを集約できる})

finally()

try...catch構文におけるfinallyブロック相当の機能を提供している。すなわち、非同期処理が成功したかどうかに関わらず、Promiseインスタンスがsettled状態になった時実行されるコールバックを登録できます。

constonFinally=Promise.resolve().finally(()=>console.log('finallyのコールバック'));console.log(onFinally);>>>Promise{<pending>}finallyのコールバック

catch()と異なり、finally()のコールバックの戻り値はPromiseインスタンスが解決される値に影響しません。

constreturnValueInFinally=Promise.resolve(1).finally(()=>2)setTimeout(()=>console.log(returnValueInFinally),1000)>>>Promise{1}

異常系ではコールバックの挙動が返されるPromiseインスタンスに影響します。
コールバックないでエラーがthrowされる場合や、コールバックがrejectedなPromiseインスタンスを返す場合、finally()の返すPromiseインスタンスも同じ理由で拒否されます。

constthrowErrorInFinally=Promise.resolve(1).finally(()=>{thrownewError('エラー')});setTimeout(()=>console.log(throwErrorInFinally),1000)>>>(node:9673)UnhandledPromiseRejectionWarning:Error:エラー...(省略)Promise{<rejected>Error:エラー...(省略)

コールバックの戻り値がPromiseインスタンスの場合、finally()の返すPromiseインスタンスはコールバックの返すPromiseインスタンスが解決されるまで解決されません。

Promise.resolve('foo').finally(()=>newPromise(resolve=>setTimeout(()=>{console.log('finally() 1秒経過')resolve()},1000))).then(console.log)>>>finally()1秒経過foo

Promiseのスタティックメソッドを自用した並行実行

Promise.all()

Promise.all()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが全てfulfilledになった時fulfilledになり、1つでもrejectedになるとその他のPromiseインスタンスの結果を待たずにrejectedになります。

// 正常系constallResolved=Promise.all([1,Promise.resolve('foo'),Promise.resolve(true)])setTimeout(()=>console.log(allResolved),1000)>>>Promise{[1,'foo',true]}
// 異常系constcontainRejected=Promise.all([1,Promise.reject('foo'),Promise.resolve(true)])setTimeout(()=>console.log(containRejected),1000)>>>(node:9936)UnhandledPromiseRejectionWarning:foo...(省略)Promise{<rejected>'foo'}

複数の非同期処理を逐次実行する必要がなければ、Promise.all()により並行実行する方が処理が早くなります。

importperf_hooksfrom'perf_hooks';constasyncFunc=()=>newPromise(resolve=>setTimeout(resolve,1000))conststart=perf_hooks.performance.now();asyncFunc().then(asyncFunc).then(asyncFunc).then(asyncFunc).then(()=>console.log('逐次実行所要時間',perf_hooks.performance.now()-start))Promise.all([asyncFunc(),asyncFunc(),asyncFunc(),asyncFunc()]).then(()=>console.log('並行実行所要時間',perf_hooks.performance.now()-start))>>>並行実行所要時間1002.2058520019054逐次実行所要時間4016.1091419905424

Promise.race()

Promise.race()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが1つでもsettledになると、その他のPromiseインスタンスの結果を待たずにそのPromiseインスタンスと同じ状態になります。

constwait=(time)=>newPromise(resolve=>setTimeout(resolve,time))constfulfilledFirst=Promise.race([wait(10).then(()=>1),wait(30).then(()=>'foo'),wait(20).then(()=>Promise.reject(newError('エラー')))])constrejectFirst=Promise.race([wait(20).then(()=>1),wait(30).then(()=>'foo'),wait(10).then(()=>Promise.reject(newError('エラー')))])constcontainNonPromise=Promise.race([wait(10).then(()=>1),'foo',wait(20).then(()=>Promise.reject(newError('エラー')))])setTimeout(()=>console.log(fulfilledFirst),1000)setTimeout(()=>console.log(rejectFirst),1000)setTimeout(()=>console.log(containNonPromise),1000)>>>Promise{1}Promise{<rejected>Error:エラー...(省略)}Promise{'foo'}

引数にから配列を渡すと、解決されることのないPromiseインスタンスを返します。

constraceWithEmptyArray=Promise.race([]);setTimeout(()=>console.log(raceWithEmptyArray),1000);>>>Promise{<pending>}

Promise.allSettled()

Promise.allSettled()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが全てsettledになったときfulfilledになります。

constallSettled=Promise.allSettled([1,Promise.resolve('foo'),Promise.reject(newError('エラー')),Promise.resolve(true)])setTimeout(()=>console.log(allSettled),1000>>>Promise{[{status:'fulfilled',value:1},{status:'fulfilled',value:'foo'},{status:'rejected',reason:Error:エラー},{status:'fulfilled',value:true}]}

Viewing all articles
Browse latest Browse all 8909

Trending Articles