Избегайте использования метода context.sync в циклах

Примечание.

В этой статье предполагается, что вы находитесь за пределами начальной стадии работы по крайней мере с одним из четырех api JavaScript для Приложений Office (для Excel, Word, OneNote и Visio), которые используют пакетную систему для взаимодействия с документом Office. В частности, вы должны знать, что делает вызов context.sync , и вы должны знать, что такое объект коллекции. Если вы не находитесь на этом этапе, начните с раздела Общие сведения об API JavaScript для Office и документации, связанной с разделом "конкретное приложение" в этой статье.

Для некоторых сценариев программирования в надстройках Office, использующих одну из моделей API для приложений (для Excel, Word, PowerPoint, OneNote и Visio), коду необходимо читать, записывать или обрабатывать некоторые свойства из каждого члена объекта коллекции. Например, надстройка Excel, которая должна получить значения каждой ячейки в определенном столбце таблицы, или надстройка Word, которая должна выделять каждый экземпляр строки в документе. Необходимо выполнить итерацию по элементам в свойстве items объекта коллекции, но из соображений производительности необходимо избегать вызовов context.sync в каждой итерации цикла. Каждый вызов context.sync — это круговой путь от надстройки к документу Office. Неоднократные круговые пути наносят ущерб производительности, особенно если надстройка работает в Office в Интернете потому что круговые пути проходят через Интернет.

Примечание.

Во всех примерах в этой статье используются 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

Запись в документ

В простейшем случае вы пишете только члены объекта коллекции, а не считываете их свойства. Например, следующий код выделяет желтым цветом каждый экземпляр "the" в документе Word.

Примечание.

Как правило, рекомендуется поставить окончательный context.sync непосредственно перед закрывающим символом "}" функции приложения run (например Excel.run, , Word.runи т. д.). Это связано с тем, что run функция выполняет скрытый вызов как последнее, что она делает, если и только в том случае, если есть команды в очереди, которые еще не были синхронизированы context.sync . Тот факт, что этот вызов скрыт, может сбить с толку, поэтому обычно рекомендуется добавить явный context.sync. Однако, учитывая, что эта статья посвящена минимизации вызовов context.sync, на самом деле более запутанным добавление совершенно ненужного конечного context.sync. Таким образом, в этой статье мы оставим runего без исключения, если в конце нет несинхронизированных команд.

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.");
})

Предыдущий код занял 1 полную секунду, чтобы завершить в документе с 200 экземплярами "the" в Word в Windows. Но когда await context.sync(); строка внутри цикла закомментирована и та же строка сразу после раскомментации цикла, операция заняла только 1/10 секунды. В Word в Интернете (с Edge в качестве браузера) потребовалось 3 полных секунды с синхронизацией внутри цикла и только 6/10 секунд с синхронизацией после цикла, примерно в пять раз быстрее. В документе с 2000 экземплярами "the" потребовалось (в Word в Интернете) 80 секунд с синхронизацией внутри цикла и только 4 секунды с синхронизацией после цикла, что примерно в 20 раз быстрее.

Примечание.

Стоит спросить, будет ли версия синхронизации внутри цикла выполняться быстрее, если синхронизация выполнялась одновременно, что можно сделать, просто удалив await ключевое слово из передней части context.sync(). Это приведет к тому, что среда выполнения инициирует синхронизацию, а затем немедленно запускает следующую итерацию цикла, не дожидаясь завершения синхронизации. Однако это не так хорошо, как перемещение context.sync из цикла полностью по этим причинам.

  • Так же, как команды в пакетном задании синхронизации помещаются в очередь, сами пакетные задания помещаются в очередь в Office, но Office поддерживает не более 50 пакетных заданий в очереди. Все больше активирует ошибки. Таким образом, если в цикле более 50 итераций, существует вероятность превышения размера очереди. Чем больше итераций, тем больше вероятность этого.
  • "Одновременно" не означает одновременно. Выполнение нескольких операций синхронизации по-прежнему занимает больше времени, чем выполнение одной.
  • Одновременные операции не гарантированы для выполнения в том же порядке, в котором они были запущены. В предыдущем примере не имеет значения, в каком порядке будет выделено слово "the", но существуют сценарии, в которых важно, чтобы элементы в коллекции обрабатывались по порядку.

Чтение значений из документа с помощью шаблона циклов разделения

Избегание context.syncs внутри цикла становится более сложной задачей, когда код должен считывать свойство элементов коллекции при обработке каждого из них. Предположим, что коду необходимо выполнить итерацию всех элементов управления содержимым в документе 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++) {
      const paragraph = contentControls.items[i].getRange('Whole').paragraphs.getFirst();
      paragraph.load('text');
      await context.sync();
      console.log(paragraph.text);
    }
});

В этом сценарии, чтобы избежать наличия context.sync в цикле, следует использовать шаблон, который мы называем шаблоном разделенного цикла . Давайте рассмотрим конкретный пример шаблона, прежде чем приступить к его формальному описанию. Вот как можно применить шаблон цикла разделения к предыдущему фрагменту кода. Обратите внимание на указанные ниже аспекты этого кода.

  • Теперь есть два цикла и context.sync приходит между ними, поэтому в любом из них нет context.sync .
  • Первый цикл выполняет итерацию по элементам в объекте коллекции и загружает text свойство так же, как и исходный цикл, но первый цикл не может регистрировать текст абзаца context.sync , так как он больше не содержит объект для заполнения text свойства paragraph прокси-объекта. Вместо этого он добавляет объект в paragraph массив.
  • Второй цикл выполняет итерацию по массиву, созданному первым циклом, и регистрирует каждый paragraph элемент в журналеtext. Это возможно, так как элемент , 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. Create первый цикл, чтобы выполнить итерацию по коллекции и добавить каждый элемент в массив, а также загрузить любое свойство элемента, которое необходимо прочитать в коде.
  3. После первого цикла вызовите context.sync для заполнения объектов прокси-сервера любыми загруженными свойствами.
  4. context.sync Следуйте за вторым циклом, чтобы выполнить итерацию по массиву, созданному в первом цикле, и считывать загруженные свойства.

Обработка объектов в документе с помощью шаблона коррелированных объектов

Рассмотрим более сложный сценарий, в котором для обработки элементов в коллекции требуются данные, которые не содержатся в самих элементах. Сценарий предусматривает Word надстройку, которая работает с документами, созданными на основе шаблона, с некоторым стандартным текстом. В тексте разбросаны один или несколько экземпляров следующих заполнителей: "{Coordinator}", "{Deputy}" и "{Manager}". Надстройка заменяет каждый заполнитель именем какого-то человека. Пользовательский интерфейс надстройки не важен для этой статьи. Например, она может иметь область задач с тремя текстовыми полями, каждое из которых отмечено одним из заполнителей. Пользователь вводит имя в каждое текстовое поле, а затем нажимает кнопку Заменить . Обработчик кнопки создает массив, который сопоставляет имена с заполнителями, а затем заменяет каждый заполнитель назначенным именем.

Вам не нужно создавать надстройку с этим пользовательским интерфейсом, чтобы поэкспериментировать с кодом. Для создания прототипа важного кода можно использовать средство Script Lab. Используйте следующую инструкцию присваивания для создания массива сопоставления.

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

В следующем коде показано, как можно заменить каждый заполнитель назначенным именем, если вы использовали context.sync внутри циклов.

Word.run(async (context) => {

    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 (на самом деле два из них) во внешнем цикле. В следующем коде показано, как удалить context.sync из циклов. Мы обсудим код позже.

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();
});

Обратите внимание, что код использует шаблон цикла разделения.

  • Внешний цикл из предыдущего примера был разделен на два. (Второй цикл имеет внутренний цикл, который ожидается, так как код выполняет итерацию по набору заданий (или заполнителей), а внутри этого набора он выполняет итерацию по соответствующим диапазонам.)
  • Есть после каждого основного context.sync цикла, но нет context.sync ни в одном цикле.
  • Второй основной цикл выполняет итерацию по массиву, созданному в первом цикле.

Но массив, созданный в первом цикле, не содержит только объект Office, как это было в первом цикле в разделе Чтение значений из документа с шаблоном разделенного цикла. Это связано с тем, что некоторые сведения, необходимые для обработки объектов range Word, не содержатся в самих объектах Range, а поступают из массиваjobMapping.

Таким образом, объекты в массиве, созданном в первом цикле, являются пользовательскими объектами с двумя свойствами. Первый представляет собой массив диапазонов Word, которые соответствуют определенному заголовку задания (то есть строке заполнителя), а второй — строке, предоставляющей имя человека, назначенного заданию. Это упрощает запись и чтение заключительного цикла, так как вся информация, необходимая для обработки заданного диапазона, содержится в том же пользовательском объекте, который содержит диапазон. Имя, которое должно заменить correlatedObject.rangesMatchingJob.items[j], является другим свойством того же объекта: correlatedObject.personAssignedToJob.

Мы называем эту вариацию шаблона разбиения цикла шаблоном коррелированных объектов . Общая идея заключается в том, что первый цикл создает массив пользовательских объектов. Каждый объект имеет свойство, значение которого является одним из элементов в объекте коллекции Office (или массиве таких элементов). Пользовательский объект имеет другие свойства, каждое из которых предоставляет сведения, необходимые для обработки объектов Office в заключительном цикле. Ссылка на пример, в котором настраиваемый объект корреляции имеет более двух свойств, см. в разделе Другие примеры этих шаблонов .

Еще один предостережение: иногда для создания массива настраиваемых коррелирующих объектов требуется несколько циклов. Это может произойти, если необходимо прочитать свойство каждого члена одного объекта коллекции Office только для сбора сведений, которые будут использоваться для обработки другого объекта коллекции. (Например, код должен считывать заголовки всех столбцов в таблице Excel, так как надстройка будет применять числовой формат к ячейкам некоторых столбцов на основе заголовка этого столбца.) Но вы всегда можете держать context.syncs между циклами, а не в цикле. Пример см. в разделе Другие примеры этих шаблонов .

Другие примеры этих шаблонов

Когда не следует использовать шаблоны, приведенные в этой статье?

Excel не может прочитать более 5 МБ данных в заданном вызове context.sync. Если это ограничение превышено, возникает ошибка. (Дополнительные сведения см. в разделе "Надстройки Excel" статьи Ограничения ресурсов и оптимизация производительности для надстроек Office .) Очень редко это ограничение приближается, но если есть вероятность того, что это произойдет с вашей надстройкой, код не должен загружать все данные в одном цикле и следовать циклу context.syncс помощью . Но по-прежнему следует избегать того, чтобы в каждой context.sync итерации цикла поверх объекта коллекции не было. Вместо этого определите подмножества элементов в коллекции и выполните цикл между циклами context.sync по каждому подмножеству. Его можно структурировать с помощью внешнего цикла, который выполняет итерацию по подмножествам и содержит context.sync в каждой из этих внешних итераций.