JavaScript
Node.js
Chrome
ECMAScript
v8
Node.jsDay 2

V8 6.3で追加されたECMAScriptの機能 (Dynamic import、Asynchronous Iteration、 Promise.prototype.finally)

この記事ではV8に追加されたECMAScriptの機能を簡単に説明します。
English

V8 6.3が2017年10月25日にリリースされ、Chrome 63で使われています。(参考: V8 JavaScript Engine)
このリリースではECMAScriptのstage-3から3機能が追加されました。
そのうちNode.jsにも入るでしょう。

harmonyオプションやライブラリを使用すれば現時点(2017/12/02)でも使用可能です。
Node8、Node9またはChromeで実行できるサンプルを用意しています。

概要

Dynamic import

仕様

動的にモジュールをimportする機能です。
例えば以下のようにmoduleAまたはmoduleBの関数どちらかしか実行しない場合

if (member.isLoggedIn()) {
  // ログインしている場合はmoduleAの関数を使いたい
} else {
  // ログインしていない場合はmoduleBの関数を使いたい
}

普通に静的なimportを使うと

import moduleA from './moduleA';
import moduleB from './moduleB';

if (member.isLoggedIn()) {
  // ログインしている場合はmoduleAの関数を使いたい
  moduleA.func();
} else {
  // ログインしていない場合はmoduleBの関数を使いたい
  moduleB.func();
}

となり、両方をimportしなければいけません。

しかし、動的にimportできるようになると

if (member.isLoggedIn()) {
  // ログインしている場合はmoduleAの関数を使いたい
  import('./moduleA.js') // ここでmoduleAが読み込まれる
    .then(module => {
      module.func();
    });
} else {
  // ログインしていない場合はmoduleBの関数を使いたい
  import('./moduleB.js') // ここでmoduleBが読み込まれる
    .then(module => {
      module.func();
    });
}

と実行するモジュールだけを動的に読み込むことができます。

他にも、ループの中でimportしたりPromiseのthenの中で読み込んだりすることができます。

for (let i = 0; i < MAX; i++) {
  import(`module${i}.js`)
    .then(module => {
      module.func();
    });
}
promiseFunc()
  .then(res => {
    return import('./module.js');
  })
  .then(module => {
    module.func(data);
  });

また、これまで見てきたとおりimport()関数はPromiseオブジェクトを返すので、
async/awaitで書くこともできます。

(async() => {
  if (member.isLoggedIn()) {
    // ログインしている場合はmoduleAの関数を使いたい
    const module = await import('./moduleA.js') // ここでmoduleAが読み込まれる
    module.func();
  } else {
    // ログインしていない場合はmoduleBの関数を使いたい
    const module = await import('./moduleB.js') // ここでmoduleBが読み込まれる
    module.func();
  }
})()

Demoを用意しました。
Chrome 63以降でアクセスし、コンソールやネットワークを見ていただけると動的に読み込まれていることが確認できると思います。
コードは以下のとおりです。

(async() => {
    let count = 0;
    // 1秒ごとにhello1、hello2、hello3をランダムで読み込む
    let id = setInterval(async() => {
      const i = Math.floor(Math.random() * 3) + 1;
      const module = await import(`./import_modules/hello${i}.js`);
      module.hello();
      count++;
      if (count === 10) {
          clearInterval(id);
      }
     }, 1000);
})();

Nodeではnode-es-module-loaderにて実行するとつかえます。
(※--harmony_dynamic_importのオプションがありますが正しく動作しませんでした。゚(゚´Д`゚)゚。)
サンプルからご確認ください。

Async Iteration

仕様

a.k.a Async Iterators / Generators
非同期処理を反復実行することが可能になります。
iteratorやgeneratorに関してはこちらを参考にしてください。

例えば、非同期な関数やAPIを複数回実行したい場合以下のようにPromise.allを使って並列実行することができます。

(async() => {
  const dataList = await Promise.all([
    fetch('https://qiita.com/api/v2/tags/Node.js'),
    fetch('https://qiita.com/api/v2/tags/JavaScript'),
    fetch('https://qiita.com/api/v2/tags/npm'),
  ]);
  for (const data of dataList) {
    console.log(data);
  }
})();

しかし、実行したい関数やAPIが増えると対応しきれなくなります。
反復処理で対応できる場合、async iteratorsを使うと以下のように書けます。

(async() => {
  const promises = [];
  for (const tag of ["Node.js", "JavaScript", "npm"]) {
    promises.push(fetch(`https://qiita.com/api/v2/tags/${tag}`));
  }

  for await (const data of promises) {
    console.log(data)
  }
})();

さらにasync generatorsを使うと無限ループに対する独自のイテレーションを行えます。
Demoを用意しました。
Chrome 63以降でアクセスし、コンソールやネットワークを見ていただけるとAPIが実行されていることが確認できると思います。

コードは以下のとおりです。HTTPクライアントにはaxiosを使ってます。

/*
 * 乱数を取得
 */
async function* gen() {
  while (true) {
    const res = await axios.get('https://www.random.org/decimal-fractions/?num=1&dec=10&col=1&format=plain&rnd=new');
    const num = res.data;
    yield Number(num);
  }
}

(async() => {
  const BREAK = 0.8;
  for await (const num of gen()) {
    console.log(num);
    if (num > BREAK) {
      console.log("over", BREAK);
      break;
    }
  }
})();

Nodeでは--harmony_async_iterationオプションを付けて実行すると使えます。
サンプルからご確認ください。

Promise.prototype.finally

仕様

try-catch-finallyのfinallyです。
finallyに関しては説明不要かと思います。
エラーになってもならなくても必ず実行してほしい処理はthencatchの両方に書かなければいけませんでした。

promiseFunc()
  .then(() => {
    someFunction();
    closeFunction(); // 成功した場合用
  })
  .catch(err => {
    console.log(err);
    closeFunction(); // エラーになった場合用
  });

しかし、普通にtry-catch-finallyで書けるようになります。

promiseFunc()
  .then(() => {
    someFunction();
  })
  .catch(err => {
    console.log(err);
  })
  .finally(() => {
    closeFunction(); // 必ず実行される
  });

ちなみにasync/awaitを使えば現在でもtry-catch-finallyは使えます。

(async() => {
  try {
    await promiseFunc();
    await someFunction();
  } catch (err) {
    console.log(err);
  } finally {
    closeFunction();
  }
})();

こちらもDemoを用意しました。
以下コードです。

Promise.resolve("resolve")
  .then(val => {console.log(val);return "then"})
  .then(val => {console.log(val);throw new Error("catch")})
  .catch(err => {console.log(err.message)})
  .finally(() => {console.log("finally")});

Chrome 63以降でアクセスし、コンソールを見ていただけるとresolve()then()catch()finally()の処理が実行されていることが確認できると思います。

Nodeでは--harmony_promise_finallyオプションを付けて実行すると使えます。
サンプルからご確認ください。

まとめ

  • ECMAScriptの新機能がV8に入った
  • Chromeでは63以降使える
  • V8には実装されたのでそのうちNodeにも入るNode v10に入ります

参考

謝辞

この記事を書くにあたって@shimataro999にレビューしていただきました。
この場を借りてお礼申し上げます。ありがとうございました。

最後までお読み頂きありがとうございました。
質問や不備等はコメントにてお願い致します。