試行錯誤

仕事や趣味(スマブラ)など

async awaitでJavaScriptの非同期処理を読みやすくする

JavaScriptで非同期処理を実装していく中で、Promiseの非同期処理が入れ子になって行って読みにくくなったことがありました。
そんな時にシンタックスシュガーのasyncとawaitを知り、かなり感動したのでその紹介になります。

実装例:PDFJS

PDFJSはその名の通りPDFをJSに読み込んでページに描画することができるライブラリになります。

mozilla.github.io

下のコードは公式ドキュメントに載っているコードになります

// If absolute URL from the remote server is provided, configure the CORS
// header on that server.
var url = 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf';

// Loaded via <script> tag, create shortcut to access PDF.js exports.
var pdfjsLib = window['pdfjs-dist/build/pdf'];

// The workerSrc property shall be specified.
pdfjsLib.GlobalWorkerOptions.workerSrc = '//mozilla.github.io/pdf.js/build/pdf.worker.js';

// ①PDFのローディング
const loadingTask = pdfjsLib.getDocument(url);
loadingTask.promise.then(function(pdf) {
  console.log('PDF loaded');
  
  // Fetch the first page
  const pageNumber = 1;
  // ②ページデータのローディング
  pdf.getPage(pageNumber).then(function(page) {
    console.log('Page loaded');
    
    const scale = 1.5;
    const viewport = page.getViewport({scale: scale});

    // Prepare canvas using PDF page dimensions
    const canvas = document.getElementById('the-canvas');
    const context = canvas.getContext('2d');
    canvas.height = viewport.height;
    canvas.width = viewport.width;

    // Render PDF page into canvas context
    const renderContext = {
      canvasContext: context,
      viewport: viewport
    };
    const renderTask = page.render(renderContext);
    // ③ページデータをCanvasに描画する処理
    renderTask.promise.then(function () {
      console.log('Page rendered');
    });
  });
}, function (reason) {
  // PDF loading error
  console.error(reason);
});

上では3つの非同期処理が続いています。
①PDFをロードする処理
②PDFの指定ページデータをロードする処理
③ページデータをCanvasに描画する処理
問題は、3つ続いてpromise.thenとなっていることから可読性が悪くなっています。

async awaitを使ってPDFJSを導入してみる

上のPDFJSの使用例をasync awaitを使って実装してみると下のようになります

const loadPDF = asyc(url) => {
  // Loaded via <script> tag, create shortcut to access PDF.js exports.
  const pdfjsLib = window['pdfjs-dist/build/pdf'];

  // The workerSrc property shall be specified.
  pdfjsLib.GlobalWorkerOptions.workerSrc = '//mozilla.github.io/pdf.js/build/pdf.worker.js';

  // ①PDFのローディング
  const loadingTask = await pdfjsLib.getDocument(url);
  loadingTask.promise.then(function(pdf){
    console.log('PDF loaded');

    // Fetch the first page
    const pageNumber = 1;
    // ②ページデータのローディング
    const page =  pdf.getPage(pageNumber);
    console.log('Page loaded');
    
    const scale = 1.5;
    const viewport = page.getViewport({scale: scale});

    // Prepare canvas using PDF page dimensions
    const canvas = document.getElementById('the-canvas');
    const context = canvas.getContext('2d');
    canvas.height = viewport.height;
    canvas.width = viewport.width;

    // Render PDF page into canvas context
    const renderContext = {
       canvasContext: context,
       viewport: viewport
    };
    const renderTask = page.render(renderContext);
    // ③ページデータをCanvasに描画する処理
    await renderTask.promise;
    console.log('Page rendered');
  }, function (reason) {
    // PDF loading error
    throw new  Error(`${reason}`);
});

const main = async () => {
  try{
    // If absolute URL from the remote server is provided, configure the CORS
    // header on that server.
    const url = 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf';
    await loadPDF(url); 
  } catch(error){
    console.error(error);
  }
}

main();

解説を少し

関数宣言にasyncを付けることで、関数内の処理でawaitが使えるようになります。今回の場合は、loadPDF関数を作りその関数定義にasyncを付けることで、以降に続くpromiseの処理をawait呼ぶことができます。(わざわざmain()という非同期関数を作って呼んでいます)

awaitによってpromiseの返り値をそのまま変数に格納することができます。
今回の場合は、promiseで返される値を変数として格納する式を書くことができるようになるので、かなり直感的に読むことができるようになります。

しかしながら課題があり例外処理をawaitから受け取ることができないようなので、上のloadingTaskのみpromiseで残して、rejectされた時に例外を生成するようにしています。(もしこの点について誤り等ありましたら、コメント等で教えていただけたら幸いです)