ループで context.sync メソッドを使用しないでください

注:

この記事では、バッチ システムを使用して Office ドキュメントを操作する 4 つのアプリケーション固有の Office JavaScript API (Excel、Word、OneNote、Visio 用) のうち、少なくとも 1 つを使い始めることを前提としています。 特に、 context.sync の呼び出しが何をするかを知っておく必要があり、コレクション オブジェクトが何であるかを知っておく必要があります。 まだ段階でない場合は、 まず「Office JavaScript API について」 と、その資料の「アプリケーション固有」にリンクされているドキュメントを参照してください。

アプリケーション固有の API モデルのいずれかを使用する Office アドインには、コードでコレクション オブジェクトのすべてのメンバーから何らかのプロパティを読み書きする必要があるシナリオがあります。 たとえば、特定のテーブル列のすべてのセルの値を取得する Excel アドインや、ドキュメント内の文字列のすべてのインスタンスを強調表示する Word アドインなどです。 コレクション オブジェクトの items プロパティ内のメンバーを反復処理する必要がありますが、パフォーマンス上の理由から、ループの反復のたびに context.sync を呼び出すことは避けてください。 context.sync のすべての呼び出しは、アドインから Office ドキュメントへのラウンド トリップです。 ラウンド トリップがインターネット経由で送信されるため、特にアドインが Office on the web で実行されている場合は、ラウンド トリップが繰り返されるとパフォーマンスが低下します。

注:

この記事のすべての例で for ループが使用されますが、次に示す方法は、配列を反復処理できるすべてのループ ステートメントに適用されます。

  • for
  • for of
  • while
  • do while

また、関数が渡され、配列内の項目に適用されるすべての配列メソッドにも適用されます。これには、次のものが含まれます。

  • Array.every
  • Array.forEach
  • Array.filter
  • Array.find
  • Array.findIndex
  • Array.map
  • Array.reduce
  • Array.reduceRight
  • Array.some

注:

一般に、アプリケーションrun関数 (Excel.runWord.run など) の終了文字 "}" の直前に最後のcontext.syncを置くことをお勧めします。 これは、 run 関数が、まだ同期されていないキューに入れられたコマンドがある場合にのみ、最後の処理として context.sync の非表示呼び出しを行うためです。 この呼び出しが非表示になっていると混乱を招く可能性があるため、明示的な context.syncを追加することをお勧めします。 ただし、この記事が context.sync の呼び出しを最小限に抑えることについて説明していることを考えると、まったく不必要な最終 context.syncを追加する方が実際にはより混乱します。 したがって、この記事では、 runの最後に同期されていないコマンドがない場合は省略します。

ドキュメントへの書き込み

最も単純なケースでは、コレクション オブジェクトのメンバーへの書き込みのみであり、そのプロパティは読み取れません。 たとえば、次のコードは、Word ドキュメント内の "the" のすべてのインスタンスを黄色で強調表示します。

await Word.run(async function (context) {
  let startTime, endTime;
  const docBody = context.document.body;

  // search() returns an array of Ranges.
  const searchResults = docBody.search('the', { matchWholeWord: true });
  searchResults.load('font');
  await context.sync();

  // Record the system time.
  startTime = performance.now();

  for (let i = 0; i < searchResults.items.length; i++) {
    searchResults.items[i].font.highlightColor = '#FFFF00';

    await context.sync(); // SYNCHRONIZE IN EACH ITERATION
  }
  
  // await context.sync(); // SYNCHRONIZE AFTER THE LOOP

  // Record the system time again then calculate how long the operation took.
  endTime = performance.now();
  console.log("The operation took: " + (endTime - startTime) + " milliseconds.");
})

Word on Windows の "the" が 200 個含まれるドキュメントで上記のコードが完了するのに 1 秒かかりました。 しかし、ループ内の await context.sync(); 行がコメント アウトされ、ループの直後の同じ行がコメント 解除されると、操作には 1/10 秒しかかかりませんでした。 Web 上の Word (ブラウザーとして Edge を使用) では、ループ内の同期には丸 3 秒かかり、ループ後の同期にはわずか 10 分の 6 秒かかり、約 5 倍の速さでした。 「the」のインスタンスが 2000 個あるドキュメントでは、(Word on the web では) ループ内の同期に 80 秒かかり、ループ後の同期にはわずか 4 秒かかり、約 20 倍高速でした。

注:

同期が同時に実行された場合に、ループ内同期バージョンがより速く実行されるかどうかを確認する価値があります。これは、context.sync()の前面からawait キーワード (keyword)を削除するだけで実行できます。 これにより、ランタイムは同期を開始し、同期の完了を待たずにループの次の反復をすぐに開始します。 ただし、これは次の理由から、 context.sync をループから完全に外すほど優れた解決策ではありません。

  • 同期バッチ ジョブのコマンドがキューに登録されるのと同じように、バッチ ジョブ自体は Office でキューに登録されますが、Office がサポートするバッチ ジョブは 50 個以下です。 これ以上はエラーをトリガーします。 そのため、ループ内に 50 回を超える反復がある場合は、キュー サイズを超える可能性があります。 反復回数が多いほど、これが発生する可能性が高くなります。
  • "同時" とは同時にという意味ではありません。 それでも、複数の同期操作を実行する方が、1 つの同期操作を実行するよりも時間がかかります。
  • 同時操作は、開始したのと同じ順序で完了することは保証されていません。 前の例では、"the" という単語が強調表示される順序は関係ありませんが、コレクション内の項目を順番に処理することが重要なシナリオがあります。

分割ループ パターンを使用してドキュメントから値を読み取る

コードが各コレクション項目を処理するときにコレクション項目のプロパティを読み取る必要がある場合、ループ内でcontext.sync回避することがより困難になります。 コードで Word ドキュメント内のすべてのコンテンツ コントロールを繰り返し処理し、各コントロールに関連付けられている最初の段落のテキストをログに記録する必要があるとします。 プログラミングの感覚により、コントロールをループ処理し、各 (最初) の段落の text プロパティを読み込み、 context.sync を呼び出してプロキシ段落オブジェクトにドキュメントのテキストを設定し、それをログに記録します。 次に例を示します。

Word.run(async (context) => {
    const contentControls = context.document.contentControls.load('items');
    await context.sync();

    for (let i = 0; i < contentControls.items.length; i++) {
      // The sync statement in this loop will degrade performance.
      const paragraph = contentControls.items[i].getRange('Whole').paragraphs.getFirst(); 
      paragraph.load('text');
      await context.sync();
      console.log(paragraph.text);
    }
});

このシナリオでは、ループ内に context.sync が発生しないようにするには、 分割ループ パターンと呼ばれるパターンを使用する必要があります。 パターンの正式な説明に入る前に、パターンの具体的な例を見てみましょう。 分割ループ パターンを上記のコード スニペットに適用する方法を次に示します。 このコードについては、次の点に注意してください。

  • 現在は 2 つのループがあり、 context.sync はその間にあるため、どちらのループ内にも context.sync はありません。
  • 最初のループでは、元のループと同様に、コレクション オブジェクト内の項目を反復処理して text プロパティを読み込みますが、最初のループでは、paragraphプロキシ オブジェクトの text プロパティを設定するcontext.syncが存在しなくなったため、段落テキストをログに記録できません。 代わりに、配列に paragraph オブジェクトを追加します。
  • 2 番目のループは、最初のループで作成された配列を反復処理し、各paragraph項目のtextを記録します。 これが可能なのは、2 つのループ間にある context.sync によってすべての text プロパティが設定されているためです。
Word.run(async (context) => {
    const contentControls = context.document.contentControls.load("items");
    await context.sync();

    const firstParagraphsOfCCs = [];
    for (let i = 0; i < contentControls.items.length; i++) {
      const paragraph = contentControls.items[i].getRange('Whole').paragraphs.getFirst();
      paragraph.load('text');
      firstParagraphsOfCCs.push(paragraph);
    }

    await context.sync();

    for (let i = 0; i < firstParagraphsOfCCs.length; i++) {
      console.log(firstParagraphsOfCCs[i].text);
    }
});

前の例では、 context.sync を含むループを分割ループ パターンに変換するための次の手順を示します。

  1. ループを 2 つのループに置き換えます。
  2. 最初のループを作成してコレクションを反復処理し、各項目を配列に追加すると同時に、コードが読み取る必要がある項目のプロパティを読み込みます。
  3. context.sync を使用して最初のループの後に、読み込まれたプロパティをプロキシ オブジェクトに設定します。
  4. 2 番目のループを使用して context.sync に従って、最初のループで作成された配列を反復処理し、読み込まれたプロパティを読み取ります。

相関オブジェクト パターンを使用してドキュメント内のオブジェクトを処理する

コレクション内のアイテムを処理するためにアイテム自体にないデータが必要になる、より複雑なシナリオを考えてみましょう。 このシナリオでは、定型文を含むテンプレートから作成されたドキュメントで動作する Word アドインを想定しています。 テキスト中には、"{Coordinator}"、"{Deputy}"、"{Manager}" というプレースホルダー文字列が 1 つ以上散在しています。 各プレースホルダーは、アドインによって一定のユーザーの名前に置き換えられます。 この記事にとってアドインの UI は重要ではありませんが、アドインには 3 つのテキスト ボックスを含む作業ウィンドウがあり、それぞれにいずれかのプレースホルダーのラベルが付けられています。 ユーザーは各テキスト ボックスに名前を入力し、[ 置換] ボタンを押します。 ボタンのハンドラーは、名前をプレースホルダーにマップする配列を作成し、各プレースホルダーを割り当てられた名前に置き換えます。

Script Lab ツールを使用して、ここに示すコード スニペットに従うことができます。 Word では、"関連付けられたオブジェクト パターン" サンプルを読み込むか、GitHub リポジトリからこのサンプル コードをインポートできます

次の代入文は、プレースホルダー名と割り当てられた名前の間のマッピング配列を作成します。

const jobMapping = [
        { job: "{Coordinator}", person: "Sally" },
        { job: "{Deputy}", person: "Bob" },
        { job: "{Manager}", person: "Kim" }
    ];

次のコードは、ループ内で context.sync を使用した場合に、各プレースホルダーを割り当てられた名前に置き換える方法を示しています。 これは、サンプルの replacePlaceholdersSlow 関数に対応します。

Word.run(async (context) => {
    // The context.sync calls in the loops will degrade performance.
    for (let i = 0; i < jobMapping.length; i++) {
      let options = Word.SearchOptions.newObject(context);
      options.matchWildcards = false;
      let searchResults = context.document.body.search(jobMapping[i].job, options);
      searchResults.load('items');

      await context.sync(); 

      for (let j = 0; j < searchResults.items.length; j++) {
        searchResults.items[j].insertText(jobMapping[i].person, Word.InsertLocation.replace);

        await context.sync();
      }
    }
});

上記のコードには、外側のループと内部のループがあります。 それぞれに context.sync 呼び出しが含まれています。 この記事の最初のコード スニペットから、内部ループの context.sync は内部ループの後に簡単に移動できることがわかります。 しかし、それでも、外側のループに context.sync (実際にはそのうちの2つ)を持つコードが残ります。 次のコードは、ループから context.sync を削除する方法を示しています。 サンプルの replacePlaceholders 関数に対応します。 コードについては後で説明します。

Word.run(async (context) => {

    const allSearchResults = [];
    for (let i = 0; i < jobMapping.length; i++) {
      let options = Word.SearchOptions.newObject(context);
      options.matchWildcards = false;
      let searchResults = context.document.body.search(jobMapping[i].job, options);
      searchResults.load('items');
      let correlatedSearchResult = {
        rangesMatchingJob: searchResults,
        personAssignedToJob: jobMapping[i].person
      }
      allSearchResults.push(correlatedSearchResult);
    }

    await context.sync()

    for (let i = 0; i < allSearchResults.length; i++) {
      let correlatedObject = allSearchResults[i];

      for (let j = 0; j < correlatedObject.rangesMatchingJob.items.length; j++) {
        let targetRange = correlatedObject.rangesMatchingJob.items[j];
        let name = correlatedObject.personAssignedToJob;
        targetRange.insertText(name, Word.InsertLocation.replace);
      }
    }

    await context.sync();
});

このコードでは分割ループ パターンが使用されていることに注意してください。

  • 前の例の外側のループが 2 つに分割されています。 (2 番目のループには内部ループがあります。これは、コードが一連のジョブ (またはプレースホルダー) を反復処理し、そのセット内で一致する範囲を反復処理するためです。)
  • 各メジャー ループの後には context.sync がありますが、どのループ内にも context.sync はありません。
  • 2 番目の主要ループは、最初のループで作成された配列を反復処理します。

ただし、最初のループで作成された配列には、「分割ループ パターンを使用してドキュメントから値を読み取る」セクションの最初のループのように、Office オブジェクトだけが含まれているわけではありません。 これは、Word Range オブジェクトを処理するために必要な情報の一部が Range オブジェクト自体になく、代わりに jobMapping 配列から取得されるためです。

そのため、最初のループで作成された配列内のオブジェクトは、2 つのプロパティを持つカスタム オブジェクトです。 1 つ目は、特定の役職に一致する Word 範囲の配列 (プレースホルダー文字列) で、2 つ目は、その職種に割り当てられた人の名前を提供する文字列です。 これにより、特定の範囲を処理するために必要なすべての情報が、範囲を含む同じカスタム オブジェクトに含まれているため、最終的なループは書き込みも読み取りも容易になります。 correlatedObject.rangesMatchingJob.items[j]を置き換える名前は、同じオブジェクトの別のプロパティであるcorrelatedObject.personAssignedToJobです。

この分割ループ パターンのバリエーションを、 相関オブジェクト パターンと呼びます。 一般的な考え方として、最初のループでカスタム オブジェクトの配列が作成されます。 各オブジェクトには、Office コレクション オブジェクト (またはそうした項目の配列) 内の項目の 1 つを値とするプロパティがあります。 カスタム オブジェクトには他のプロパティがあり、それぞれが最終的なループで Office オブジェクトを処理するために必要な情報を提供します。 カスタム相関オブジェクトに 2 つ以上のプロパティがある例へのリンクについては、「 これらのパターンのその他の例 」セクションを参照してください。

もう 1 つの注意事項: カスタム相関オブジェクトの配列を作成するために複数のループが必要になる場合があります。 これは、1 つの Office コレクション オブジェクトの各メンバーのプロパティを読み取る必要があるとき、その情報を収集し、そのコレクション オブジェクトが別のコレクション オブジェクトを処理する必要がある場合に発生することがあります。 (たとえば、アドインでは列のタイトルに基づいて一部の列のセルに数値書式を適用するため、コードでは Excel のテーブルのすべての列のタイトルを読み取る必要があります)。ただし、 context.sync は、ループ内ではなく、ループ間でいつでも保持できます。 例については、「 これらのパターンのその他の例 」セクションを参照してください。

これらのパターンのその他の例

  • Array.forEachループを使用するExcelの非常に簡単な例については、このStack Overflowの質問に対する受け入れられた回答を参照してください:context.syncの前に複数のcontext.loadをキューに入れることは可能ですか?
  • Array.forEach ループを使用し、async/await構文を使用しない Word の簡単な例については、次の Stack Overflow の質問に対する受け入れられた回答を参照してください。Office JavaScript API を使用してコンテンツ コントロールを使用してすべての段落を反復処理する
  • 高度な Word サンプルの場合は、この要点Script Lab ツールにインポートします。 要点を使用する際のコンテキストについては、Stack Overflow の質問「 テキストを置換した後、ドキュメントが同期しない」に対する受け入れられた回答を参照してください。 このサンプルでは、3 つのプロパティを持つカスタム相関オブジェクトの種類を作成します。 合計 3 つのループを使用して、相関するオブジェクトの配列を構築し、さらに 2 つのループを使用して最終処理を行います。 forループとArray.forEachループが混在しています。
  • 厳密には分割ループまたは相関オブジェクト パターンの例ではありませんが、1 回の context.syncで一連のセル値を他の通貨に変換する方法を示す高度な Excel サンプルがあります。 試すには、Script Lab ツールを開き、Currency Converter サンプルを検索して移動します。

この記事で示しているパターンを使 わないべきな のはどのような場合ですか?

Excel on the web は、特定の context.sync 呼び出しで 5 MB を超えるデータを読み取ることはできません。 この制限を超えた場合は、エラーがスローされます。 (詳細については、「 Office アドインのリソース制限とパフォーマンスの最適化 」の「Excel アドイン」セクションを参照してください。)この制限に近づくことはまれですが、アドインでこれが発生する可能性がある場合は、コードですべてのデータを 1 つのループ に読み込んでcontext.syncでループをたどるのをお勧めしません。 ただし、それでも、コレクション オブジェクトのループの反復ごとに context.sync が生じないようにする必要があります。 代わりに、コレクション内の項目のサブセットを定義し、ループ間を context.sync して各サブセットを順番にループします。 サブセットを反復処理し、これらの外部反復のそれぞれに context.sync を含む外部ループを使用して、これを構造化できます。