読者です 読者をやめる 読者になる 読者になる

JS.next

JavaScriptの最新実装情報を追うブログ

async関数が実装された

★★★ ES2017 新仕様実装 V8

概要

非同期な処理を同期的に書ける関数タイプが実装された。


基本

「async」キーワードに続けて関数定義を書くと、async関数となる。

async function afn1() { }

afn2 = async () => { }

obj = { 
  async afn3() { }
}


async関数を呼び出すとプロミスが返される。

console.log( afn1() )  // <Promise>


このプロミスは、async関数が終了するとその返り値で解決され、例外が起こると棄却される。

async function afn4( flag ) {
  if ( flag ) return 'Yes'
  else throw 'No'
}

afn4( true  ).then(  v => console.log( v ) )  /// "Yes"
afn4( false ).catch( v => console.log( v ) )  /// "No"


ちなみに返り値にプロミスを返すと、その解決を待ってからその解決値をもってasync関数が返すプロミスは解決される。

async function afn5() {
  return Promise.resolve( 123 )
}

afn5().then( v => console.log( v ) )  /// 123


await式

await式はキーワード「await」とプロミス型値「p」を使って、「await p」の形で書くことが出来る。
await式は「p」(プロミスでなくともプロミスとしてラップされる)が解決するまで待ってその解決値を式結果として返す。

function delay( s ) {  // s秒後に解決するプロミスを返す関数
  return new Promise( ok => setTimeout( ok, s, `${s}秒経ったよ!` ) )
}
async function afn6() {
  console.log( 'スタート' )
  let message = await delay( 1 )
  console.log( message )
}

afn6()
  /// "スタート"  →  "1秒経ったよ!"


async関数もプロミスを返すので、async関数の処理の待ち合いにも使える。

async function afn7() {
  for ( let i = 0; i < 3; i++ ) {
    await afn6()
  }
}

afn7()
  /// "スタート"  →  "1秒経ったよ!"  →
  /// "スタート"  →  "1秒経ったよ!"  →
  /// "スタート"  →  "1秒経ったよ!"


つまり、プロミスを返す関数を利用するのにasync関数が良く使え、
async関数を利用するのにasync関数が良く使える。


応用

プロミスを返すWeb APIを良く扱う

例:キャッシュからファイルを返す関数 もし無ければフェッチしてファイルを取ってきてキャッシュに追加する
いままで:

function getTextFile( url ) {
  return caches.open( 'test' ).then( 
    cache => cache.match( url ).then(
      response => {
         if ( !response ) {
           return fetch( url ).then(
             response => cache.put( url, response )
           ).then(
             () => getTextFile( url )
           )
         }
         return response.text()          
      }
    )
  ) 
}

getTextFile( '' ).then( t => console.dir( t ) )

ややこしい。


ジェネレータを応用すると:

function spawn( gfn ) {
  return ( ...args ) => {
    const gen = gfn( ...args )
    return new Promise( ( resolve, reject ) => {
      const step = v => {
        const { value, done } = gen.next(v)
        Promise.resolve( value ).then( done ? resolve : step ).catch( reject )
      }
      step()
    } )
  }
}
getTextFile = spawn( function* ( url ) {
  const cache = yield caches.open( 'test' )  
  const response = yield cache.match( url )  
  if ( !response ) {
    yield cache.put( url, yield fetch( url ) )
    return yield getTextFile( url )
  }
  return response.text()
})

getTextFile( '' ).then( t => console.dir( t ) )

トリッキー。


async関数を使うと:

async function getTextFile( url ) {
  const cache = await caches.open( 'test' )  
  const response = await cache.match( url )  
  if ( !response ) {
    await cache.put( url, await fetch( url ) )
    return await getTextFile( url )
  }
  return response.text()
}

getTextFile( '' ).then( t => console.dir( t ) )

スマートに書ける。


非同期、同期交えた逐次処理に使う

例:フィボナッチ数列を計算する
普通に書くと:

function calcFibAry( size ) {
  let p = 1, q = 1
  const ary = [ p, q ]   
  while ( ary.length < size ) {
     [ p, q ] = [ q, p + q ]
     ary.push( q )
  }
  return ary
}

console.log( calcFibAry( 1e7 ) )

いつまでもレンダリング等へ制御が移らず固まる。


ジェネレータとrequestIdleCallbackを使うと:

function *calcFibAryStep( size ) {
  let p = 1, q = 1
  const ary = [ p, q ]   
  while ( ary.length < size ) {
     [ p, q ] = [ q, p + q ]
     ary.push( q )
     yield
  }
  return ary
}
function calcFibAryAsync( size ) {  
  return new Promise( resolve => {
    const step = calcFibAryStep( size )
    const proceed = deadline => {
      do {
        const { value, done } = step.next()
        if ( done ) resolve( value )
      } while ( deadline.timeRemaining() > 0 )
      requestIdleCallback( proceed )
    }
    requestIdleCallback( proceed )
  } )  
}

calcFibAryAsync( 1e7 ).then( a => console.log( a ) )

効率よくシンプルで美しく書くのが難しい。


async関数とrequestIdleCallbackを使うと:

async function calcFibAryAsync( size ) {
  let p = 1, q = 1
  const ary = [ p, q ] 
  const waitUntilIdle = () => new Promise( ok => requestIdleCallback( ok ) )
  let deadline = await waitUntilIdle()  
  while ( ary.length < size ) {
     [ p, q ] = [ q, p + q ]
     ary.push( q )
     if ( deadline.timeRemaining() == 0 ) deadline = await waitUntilIdle()
  }
  return ary
}

calcFibAryAsync( 1e7 ).then( a => console.log( a ) )

固まらず、スマートに書ける。


実装されるバージョン

V8 5.2.328