ループで 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.run
、Word.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.");
})
上記のコードは、Windows 上の Word で 200 個のインスタンスが "the" のドキュメントで完了するまでに 1 秒かかりました。 ただし、ループ内の await context.sync();
行がコメントアウトされ、ループがコメント解除された直後に同じ行が行われると、操作は 1 秒の 1/10 分の 1 しかかからなりました。 Web 上のWord (ブラウザーとして Edge を使用) では、ループ内の同期に 3 秒、ループ後の同期で 1 秒の 6/10 分の 6 分の 1 しかかからなりました(約 5 倍)。 "the" のインスタンスが 2000 個あるドキュメントでは、ループ内の同期に 80 秒(web 上のWord)、ループ後の同期で約 4 秒、約 20 倍の時間がかかりました。
注:
同期が同時に実行された場合に、sync-inside-the-loop バージョンが高速に実行されるかどうかを確認する価値があります。これは、context.sync()
の前面からawait
キーワード (keyword)を削除するだけで実行できます。 これにより、ランタイムは同期を開始し、同期の完了を待たずにループの次のイテレーションをすぐに開始します。 ただし、これは、次の理由により、 context.sync
をループから完全に移動するのと同じくらい良い解決策ではありません。
- 同期バッチ ジョブのコマンドがキューに入っているのと同様に、バッチ ジョブ自体は Office にキューに入れられますが、Office ではキュー内のバッチ ジョブは 50 個以下でサポートされます。 それ以上はエラーをトリガーします。 そのため、ループ内に 50 回を超える繰り返しがある場合は、キュー のサイズを超える可能性があります。 反復回数が多いほど、このようなことが起こる可能性が高くなります。
- "同時実行" は、同時に意味するものではありません。 複数の同期操作を実行するよりも、複数の同期操作を実行するよりも時間がかかります。
- 同時実行操作は、開始した順序と同じ順序で完了することは保証されません。 前の例では、"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
を含むループを分割ループ パターンに変換するための次の手順を示します。
- ループを 2 つのループに置き換えます。
- コレクションを反復処理する最初のループを作成し、各項目を配列に追加しながら、コードで読み取る必要がある項目のプロパティも読み込みます。
-
context.sync
で最初のループに従って、読み込まれたプロパティをプロキシ オブジェクトに設定します。 - 2 番目のループで
context.sync
に従って、最初のループで作成された配列を反復処理し、読み込まれたプロパティを読み取ります。
関連付けられたオブジェクト パターンを使用してドキュメント内のオブジェクトを処理する
コレクション内の項目を処理するには、アイテム自体にないデータが必要になる、より複雑なシナリオを考えてみましょう。 このシナリオでは、テンプレートから作成されたドキュメントに対して、定型テキストを使用して操作するWord アドインを想定しています。 テキストに散らばっているのは、"{コーディネーター}"、"{Deputy}"、"{Manager}" のプレースホルダー文字列の 1 つ以上のインスタンスです。 アドインは、各プレースホルダーを一部のユーザーの名前に置き換えます。 この記事ではアドインの UI は重要ではありませんが、アドインには 3 つのテキスト ボックスがある作業ウィンドウがあり、それぞれにプレースホルダーの 1 つでラベルが付けられます。 ユーザーは、各テキスト ボックスに名前を入力し、[ 置換 ] ボタンを押します。 ボタンのハンドラーは、名前をプレースホルダーにマップする配列を作成し、各プレースホルダーを割り当てられた名前に置き換えます。
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] を置き換える必要がある名前は、同じオブジェクトのもう 1 つのプロパティである、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 を使用してコンテンツ コントロールを使用してすべての段落を反復処理します。 - TypeScript で記述されたWordの例については、アドイン Angular2 スタイル チェッカー (特にファイル word.document.service.ts) Wordサンプルを参照してください。 これには、
for
ループとArray.forEach
ループが混在しています。 - 高度なWordサンプルについては、この gist を Script Lab ツールにインポートします。 gist の使用に関するコンテキストについては、テキストの置換後に Stack Overflow の質問 Document が同期していないという問題に対して受け入れられた回答を参照してください。 このサンプルでは、3 つのプロパティを持つカスタム相関オブジェクト型を作成します。 合計 3 つのループを使用して相関オブジェクトの配列を構築し、さらに 2 つのループを使用して最終的な処理を行います。
for
ループとArray.forEach
ループが混在しています。 - 分割ループや相関オブジェクトパターンの例ではありませんが、セル値のセットを単一の
context.sync
で他の通貨に変換する方法を示す高度な Excel サンプルがあります。 試すには、Script Lab ツールを開き、Currency Converter サンプルを検索して移動します。
この記事のパターンを使用 すべきでない のはいつですか?
Excel では、 context.sync
の呼び出しで 5 MB を超えるデータを読み取ることはできません。 この制限を超えると、エラーがスローされます。 (詳細については、「Office アドインのリソース制限とパフォーマンスの最適化」の「Excel アドイン」セクションを参照してください)。この制限に近づくことは非常にまれですが、アドインでこれが発生する可能性がある場合は、コードですべてのデータを 1 つのループに読み込み、context.sync
でループに従うべきではありません。 ただし、コレクション オブジェクトに対するループのすべてのイテレーションで context.sync
が発生することは避ける必要があります。 代わりに、コレクション内の項目のサブセットを定義し、ループ間に context.sync
を使用して、各サブセットをループします。 これは、サブセットを反復処理し、これらの外側の反復処理のそれぞれに context.sync
を含む外部ループで構成できます。
Office Add-ins