Evite usar o método context.sync em loops

Observação

Este artigo supõe que você já passou do estágio inicial de trabalho com pelo menos uma das quatro APIs JavaScript do Office específicas do aplicativo — para Excel, Word, OneNote e Visio — que usam um sistema em lotes para interagir com o documento do Office. Em particular, você deve saber o que uma chamada faz context.sync e o que é um objeto de coleção. Se você não estiver nesse estágio, comece com Noções básicas sobre a API JavaScript do Office e a documentação vinculada em "específico do aplicativo" nesse artigo.

Os suplementos do Office que usam um dos modelos de API específicos do aplicativo podem ter cenários que exigem que seu código leia ou escreva alguma propriedade de cada membro de um objeto de coleção. Por exemplo, um suplemento do Excel que obtém os valores de cada célula em uma coluna de tabela específica ou um suplemento Word que realça cada instância de uma cadeia de caracteres no documento. Você precisará iterar sobre os membros na items propriedade do objeto de coleção; mas, por motivos de desempenho, você deve evitar chamar context.sync cada iteração do loop. Cada chamada de context.sync é uma viagem de ida e volta do suplemento para o documento do Office. Viagens de ida e volta repetidas prejudicam o desempenho, especialmente se o suplemento estiver sendo executado no Office na Web porque as viagens de ida e volta passam pela Internet.

Observação

Todos os exemplos neste artigo usam for loops, mas as práticas descritas se aplicam a qualquer instrução de loop que possa iterar por meio de uma matriz, incluindo o seguinte:

  • for
  • for of
  • while
  • do while

Eles também se aplicam a qualquer método de matriz para o qual uma função é passada e aplicada aos itens na matriz, incluindo o seguinte:

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

Observação

Geralmente, é uma boa prática colocar um context.sync final logo antes do caractere "}" de fechamento da função do aplicativo run (como Excel.run, Word.run, etc.). Isso ocorre porque a run função faz uma chamada oculta como context.sync a última coisa que faz se, e somente se, houver comandos enfileirados que ainda não foram sincronizados. O fato de essa chamada estar oculta pode ser confuso, portanto, geralmente recomendamos que você adicione o .context.sync No entanto, dado que este artigo é sobre como minimizar chamadas de context.sync, é realmente mais confuso adicionar um final context.synctotalmente desnecessário . Portanto, neste artigo, deixamos de fora quando não há comandos não sincronizados no final do run.

Gravando no documento

No caso mais simples, você está gravando apenas para membros de um objeto de coleção, não lendo suas propriedades. Por exemplo, o código a seguir realça em amarelo todas as ocorrências de "o" em um documento do Word.

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

O código anterior levou 1 segundo inteiro para ser concluído em um documento com 200 instâncias de "o" no Word no Windows. Mas quando a await context.sync(); linha dentro do loop é comentada e a mesma linha logo após o loop é descomentada, a operação leva apenas 1/10 de segundo. No Word na web (com o Edge como navegador), demorou 3 segundos completos com a sincronização dentro do loop e apenas 6/10 de segundo com a sincronização após o loop, cerca de cinco vezes mais rápido. Em um documento com 2000 instâncias de "o", demorou (no Word na web) 80 segundos com a sincronização dentro do loop e apenas 4 segundos com a sincronização após o loop, cerca de 20 vezes mais rápido.

Observação

Vale a pena perguntar se a versão sincronizada dentro do loop seria executada mais rapidamente se as sincronizações fossem executadas simultaneamente, o que poderia ser feito simplesmente removendo a await palavra-chave da frente do context.sync(). Isso faria com que o tempo de execução iniciasse a sincronização e, em seguida, iniciasse imediatamente a próxima iteração do loop sem aguardar a conclusão da sincronização. No entanto, essa não é uma solução tão boa quanto sair context.sync totalmente do circuito pelos seguintes motivos.

  • Assim como os comandos em um trabalho em lotes de sincronização são enfileirados, os próprios trabalhos em lotes são enfileirados no Office, mas o Office não dá suporte a mais de 50 trabalhos em lotes na fila. Mais alguma coisa desencadeia erros. Portanto, se houver mais de 50 iterações em um loop, há uma chance de que o tamanho da fila seja excedido. Quanto maior o número de iterações, maior a chance de isso acontecer.
  • "Ao mesmo tempo" não significa simultaneamente. Ainda levaria mais tempo para executar várias operações de sincronização do que para executar uma.
  • Não há garantia de que as operações simultâneas sejam concluídas na mesma ordem em que começaram. No exemplo anterior, não importa em que ordem a palavra "o" seja realçada, mas há cenários em que é importante que os itens da coleção sejam processados em ordem.

Ler valores do documento com o padrão de loop dividido

Evitar context.sync dentro de um loop torna-se mais desafiador quando o código deve ler uma propriedade dos itens da coleção à medida que processa cada um. Suponha que seu código precise iterar todos os controles de conteúdo em um documento do Word e registrar em log o texto do primeiro parágrafo associado a cada controle. Seus instintos de programação podem levá-lo a percorrer os controles, carregar a text propriedade de cada (primeiro) parágrafo, chamar context.sync para preencher o objeto de parágrafo proxy com o texto do documento e, em seguida, registrá-lo. Apresentamos um exemplo a seguir.

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

Nesse cenário, para evitar ter um context.sync em um loop, você deve usar um padrão que chamamos de padrão de loop dividido . Vamos ver um exemplo concreto do padrão antes de chegarmos a uma descrição formal dele. Veja como o padrão de loop dividido pode ser aplicado ao trecho de código anterior. Observe o seguinte sobre este código.

  • Agora existem dois loops e o context.sync vem entre eles, então não context.sync há dentro de nenhum dos loops.
  • O primeiro loop é iterado pelos itens no objeto de coleção e carrega a text propriedade, assim como o loop original, mas o primeiro loop não pode registrar o texto do parágrafo porque não contém mais um context.sync para preencher a text propriedade do objeto proxy paragraph . Em vez disso, ele adiciona o paragraph objeto a uma matriz.
  • O segundo loop itera pela matriz que foi criada pelo primeiro loop e registra a text de cada paragraph item. Isso é possível porque o context.sync que veio entre os dois loops preencheu todas as text propriedades.
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);
    }
});

O exemplo anterior sugere o procedimento a seguir para transformar um loop que contém a context.sync no padrão de loop dividido.

  1. Substitua o laço por dois laços.
  2. Crie um primeiro loop para iterar sobre a coleção e adicionar cada item a uma matriz ao mesmo tempo em que carrega qualquer propriedade do item que seu código precisa ler.
  3. Siga o primeiro loop com context.sync para preencher os objetos proxy com quaisquer propriedades carregadas.
  4. Siga com context.sync um segundo loop para iterar sobre a matriz criada no primeiro loop e ler as propriedades carregadas.

Processar objetos no documento com o padrão de objetos correlacionados

Vamos considerar um cenário mais complexo em que o processamento dos itens na coleção requer dados que não estão nos próprios itens. O cenário prevê um suplemento do Word que opera em documentos criados a partir de um modelo com algum texto clichê. Espalhadas no texto estão uma ou mais instâncias das seguintes cadeias de caracteres de espaço reservado: "{Coordenador}", "{Adjunto}" e "{Gerente}". O suplemento substitui cada espaço reservado pelo nome de alguma pessoa. Embora a interface do usuário do suplemento não seja importante para este artigo, o suplemento pode ter um painel de tarefas com três caixas de texto, cada uma rotulada com um dos espaços reservados. O usuário insere um nome em cada caixa de texto e pressiona um botão Substituir . O manipulador do botão cria uma matriz que mapeia os nomes para os espaços reservados e, em seguida, substitui cada espaço reservado pelo nome atribuído.

Você pode usar a ferramenta Script Lab para acompanhar os trechos de código mostrados aqui. No Word, você pode carregar o exemplo "Padrão de objetos correlacionados" ou importar esse código de exemplo do repositório do GitHub.

A instrução de atribuição a seguir cria a matriz de mapeamento entre o espaço reservado e os nomes atribuídos.

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

O código a seguir mostra como você pode substituir cada espaço reservado pelo nome atribuído se tiver usado context.sync loops internos. Isso corresponde à replacePlaceholdersSlow função no exemplo.

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

No código anterior, há um loop externo e um loop interno. Cada um deles contém uma context.sync chamada. Com base no primeiro trecho de código deste artigo, você provavelmente verá que o context.sync loop interno pode simplesmente ser movido após o loop interno. Mas isso ainda deixaria o código com um context.sync (dois deles, na verdade) no loop externo. O código a seguir mostra como você pode remover context.sync dos loops. Corresponde à replacePlaceholders função no exemplo. Discutiremos o código mais tarde.

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

Observe que o código usa o padrão de loop dividido.

  • O loop externo do exemplo anterior foi dividido em dois. (O segundo loop tem um loop interno, que é esperado porque o código está sendo iterado em um conjunto de trabalhos (ou espaços reservados) e, dentro desse conjunto, está sendo iterado nos intervalos correspondentes.)
  • Há um context.sync após cada loop principal, mas não context.sync dentro de nenhum loop.
  • O segundo loop principal é iterado por meio de uma matriz criada no primeiro loop.

Mas a matriz criada no primeiro loop não contém apenas um objeto Office como o primeiro loop na seção Leitura de valores do documento com o padrão de loop dividido. Isso ocorre porque algumas das informações necessárias para processar os objetos Range do Word não estão nos objetos Range em si, mas vêm da jobMapping matriz.

Portanto, os objetos na matriz criada no primeiro loop são objetos personalizados que possuem duas propriedades. A primeira é uma matriz de intervalos do Word que correspondem a um cargo específico (ou seja, uma cadeia de caracteres de espaço reservado) e a segunda é uma cadeia de caracteres que fornece o nome da pessoa atribuída ao trabalho. Isso torna o loop final fácil de escrever e de ler, pois todas as informações necessárias para processar um determinado intervalo estão contidas no mesmo objeto personalizado que contém o intervalo. O nome que deve substituir correlatedObject.rangesMatchingJob.items[j] é a outra propriedade do mesmo objeto: correlatedObject.personAssignedToJob.

Chamamos essa variação do padrão de loop dividido de padrão de objetos correlacionados . A ideia geral é que o primeiro loop cria uma matriz de objetos personalizados. Cada objeto tem uma propriedade cujo valor é um dos itens em um objeto de coleção do Office (ou uma matriz desses itens). O objeto personalizado tem outras propriedades, cada uma das quais fornece informações necessárias para processar os objetos do Office no loop final. Consulte a seção Outros exemplos desses padrões para obter um link para um exemplo em que o objeto de correlação personalizado tem mais de duas propriedades.

Mais uma ressalva: às vezes, é necessário mais de um loop apenas para criar a matriz de objetos correlacionados personalizados. Isso pode acontecer se você precisar ler uma propriedade de cada membro de um objeto de coleção do Office apenas para coletar informações que serão usadas para processar outro objeto de coleção. (Por exemplo, seu código precisa ler os títulos de todas as colunas em uma tabela do Excel porque seu suplemento aplicará um formato de número às células de algumas colunas com base no título dessa coluna.) Mas você sempre pode manter o context.syncs entre os loops, em vez de em um loop. Consulte a seção Outros exemplos desses padrões para obter um exemplo.

Outros exemplos desses padrões

Quando você não deve usar os padrões neste artigo?

O Excel na Web não pode ler mais de 5 MB de dados em uma determinada chamada de context.sync. Se esse limite for excedido, um erro será gerado. (Consulte a "Seção de suplementos do Excel" de Limites de recursos e otimização de desempenho para Suplementos do Office para obter mais informações.) É muito raro que esse limite seja abordado, mas se houver uma chance de que isso aconteça com o suplemento, o código não deverá carregar todos os dados em um único loop e seguir o loop com um context.sync. Mas você ainda deve evitar ter um context.sync em cada iteração de um loop sobre um objeto de coleção. Em vez disso, defina subconjuntos dos itens na coleção e faça um loop sobre cada subconjunto, com um context.sync entre os loops. Você pode estruturar isso com um loop externo que itera sobre os subconjuntos e contém o context.sync em cada uma dessas iterações externas.