Evite usar el método context.sync en bucles

Nota:

En este artículo se supone que está más allá de la fase inicial de trabajo con al menos una de las cuatro API de JavaScript de Office específicas de la aplicación (para Excel, Word, OneNote y Visio) que usan un sistema por lotes para interactuar con el documento de Office. En concreto, debe saber qué hace una llamada de y debe saber qué es un objeto de context.sync colección. Si no está en esa fase, comience con Descripción de la API de JavaScript de Office y la documentación vinculada a en "específico de la aplicación" en ese artículo.

Para algunos escenarios de programación en complementos de Office que usan uno de los modelos de API específicos de la aplicación (para Excel, Word, PowerPoint, OneNote y Visio), el código debe leer, escribir o procesar alguna propiedad de cada miembro de un objeto de colección. Por ejemplo, un complemento de Excel que necesita obtener los valores de cada celda de una columna de tabla determinada o un complemento de Word que necesita resaltar cada instancia de una cadena en el documento. Debe recorrer en iteración los miembros de la items propiedad del objeto de colección; pero, por motivos de rendimiento, debe evitar llamar context.sync a en cada iteración del bucle. Cada llamada de context.sync es un recorrido de ida y vuelta desde el complemento al documento de Office. Los recorridos de ida y vuelta repetidos perjudican el rendimiento, especialmente si el complemento se ejecuta en Office en la Web porque los recorridos de ida y vuelta van a través de Internet.

Nota:

En todos los ejemplos de este artículo se usan for bucles, pero los procedimientos descritos se aplican a cualquier instrucción de bucle que pueda iterar a través de una matriz, incluido lo siguiente:

  • for
  • for of
  • while
  • do while

También se aplican a cualquier método de matriz al que se pasa una función y se aplican a los elementos de la matriz, incluido lo siguiente:

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

Escritura en el documento

En el caso más sencillo, solo se escribe en los miembros de un objeto de colección, no se leen sus propiedades. Por ejemplo, el código siguiente resalta en amarillo cada instancia de "el" en un documento Word.

Nota:

Por lo general, es recomendable colocar un final context.sync justo antes del carácter de cierre "}" de la función de aplicación run (como Excel.run, Word.run, etc.). Esto se debe a que la run función realiza una llamada oculta de context.sync como lo último que hace si, y solo si, hay comandos en cola que aún no se han sincronizado. El hecho de que esta llamada esté oculta puede ser confuso, por lo que generalmente se recomienda agregar el explícito context.sync. Sin embargo, dado que este artículo trata de minimizar las llamadas de context.sync, en realidad es más confuso agregar un final context.synctotalmente innecesario. Por lo tanto, en este artículo, lo dejamos fuera cuando no hay comandos sin sincronizar al final de 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.");
})

El código anterior tardó 1 segundo completo en completarse en un documento con 200 instancias de "el" en Word en Windows. Pero cuando se comenta la await context.sync(); línea dentro del bucle y la misma línea justo después de que se descomprime el bucle, la operación tardó solo un 1/10 de segundo. En Word en la web (con Edge como explorador), tardó 3 segundos completos con la sincronización dentro del bucle y solo 6/10ths de un segundo con la sincronización después del bucle, aproximadamente cinco veces más rápido. En un documento con 2000 instancias de "el", tardó (en Word en la web) 80 segundos con la sincronización dentro del bucle y solo 4 segundos con la sincronización después del bucle, aproximadamente 20 veces más rápido.

Nota:

Vale la pena preguntar si la versión de synchronize-inside-the-loop se ejecutaría más rápido si las sincronizaciones se ejecutaran simultáneamente, lo que se podría hacer simplemente quitando la await palabra clave de la parte frontal de context.sync(). Esto provocaría que el tiempo de ejecución iniciara la sincronización y, a continuación, iniciara inmediatamente la siguiente iteración del bucle sin esperar a que se completara la sincronización. Sin embargo, esta no es una solución tan buena como mover el context.sync bucle fuera del bucle por completo por estas razones.

  • Al igual que los comandos de un trabajo por lotes de sincronización están en cola, los propios trabajos por lotes se ponen en cola en Office, pero Office no admite más de 50 trabajos por lotes en la cola. Cualquier otro desencadenador de errores. Por lo tanto, si hay más de 50 iteraciones en un bucle, existe la posibilidad de que se supere el tamaño de la cola. Cuanto mayor sea el número de iteraciones, mayor será la posibilidad de que esto suceda.
  • "Simultáneamente" no significa simultáneamente. Seguiría tardando más en ejecutar varias operaciones de sincronización que en ejecutar una.
  • No se garantiza que las operaciones simultáneas se completen en el mismo orden en que se iniciaron. En el ejemplo anterior, no importa en qué orden se resalta la palabra "el", pero hay escenarios en los que es importante que los elementos de la colección se procesen en orden.

Leer valores del documento con el patrón de bucle dividido

context.syncEvitar s dentro de un bucle se vuelve más difícil cuando el código debe leer una propiedad de los elementos de la colección a medida que procesa cada uno de ellos. Supongamos que el código debe recorrer en iteración todos los controles de contenido de un documento Word y registrar el texto del primer párrafo asociado a cada control. Los instintos de programación pueden llevarle a recorrer en bucle los controles, cargar la text propiedad de cada párrafo (primero), llamar context.sync a para rellenar el objeto de párrafo de proxy con el texto del documento y, a continuación, registrarlo. A continuación se muestra un ejemplo.

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

En este escenario, para evitar tener un context.sync en un bucle, debe usar un patrón al que llamamos patrón de bucle dividido . Veamos un ejemplo concreto del patrón antes de obtener una descripción formal del patrón. Aquí se muestra cómo se puede aplicar el patrón de bucle dividido al fragmento de código anterior. Tenga en cuenta lo siguiente sobre este código.

  • Ahora hay dos bucles y viene context.sync entre ellos, por lo que no hay ningún context.sync bucle dentro de ninguno de ellos.
  • El primer bucle recorre en iteración los elementos del objeto de colección y carga la text propiedad igual que el bucle original, pero el primer bucle no puede registrar el texto del párrafo porque ya no contiene un context.sync objeto para rellenar la text propiedad del paragraph objeto proxy. En su lugar, agrega el paragraph objeto a una matriz.
  • El segundo bucle recorre en iteración la matriz creada por el primer bucle y registra el text de cada paragraph elemento. Esto es posible porque el context.sync que se produjo entre los dos bucles rellenaba todas las text propiedades.
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);
    }
});

En el ejemplo anterior se sugiere el procedimiento siguiente para convertir un bucle que contiene un context.sync en el patrón de bucle dividido.

  1. Reemplace el bucle por dos bucles.
  2. Create un primer bucle para recorrer en iteración la colección y agregar cada elemento a una matriz al mismo tiempo que carga cualquier propiedad del elemento que el código necesita leer.
  3. Después del primer bucle, llame context.sync a para rellenar los objetos proxy con las propiedades cargadas.
  4. Siga con context.sync un segundo bucle para recorrer en iteración la matriz creada en el primer bucle y leer las propiedades cargadas.

Procesar objetos en el documento con el patrón de objetos correlacionados

Consideremos un escenario más complejo en el que el procesamiento de los elementos de la colección requiere datos que no están en los propios elementos. El escenario prevé un complemento de Word que funciona en documentos creados a partir de una plantilla con texto reutilizable. Dispersos en el texto son una o varias instancias de las siguientes cadenas de marcador de posición: "{Coordinator}", "{Deputy}" y "{Manager}". El complemento reemplaza cada marcador de posición por el nombre de alguna persona. La interfaz de usuario del complemento no es importante para este artículo. Por ejemplo, podría tener un panel de tareas con tres cuadros de texto, cada uno etiquetado con uno de los marcadores de posición. El usuario escribe un nombre en cada cuadro de texto y, a continuación, presiona un botón Reemplazar . El controlador del botón crea una matriz que asigna los nombres a los marcadores de posición y, a continuación, reemplaza cada marcador de posición por el nombre asignado.

No es necesario generar realmente un complemento con esta interfaz de usuario para experimentar con el código. Puede usar la herramienta Script Lab para crear prototipos del código importante. Use la siguiente instrucción de asignación para crear la matriz de asignación.

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

En el código siguiente se muestra cómo se puede reemplazar cada marcador de posición por su nombre asignado si se usa dentro context.sync de bucles.

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

En el código anterior, hay un bucle externo y un bucle interno. Cada uno de ellos contiene un .context.sync En función del primer fragmento de código de este artículo, es probable que vea que en context.sync el bucle interno simplemente se puede mover después del bucle interno. Pero eso seguiría dejando el código con un context.sync (dos de ellos en realidad) en el bucle externo. En el código siguiente se muestra cómo se puede quitar context.sync de los bucles. Analizaremos el código más adelante.

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

Tenga en cuenta que el código usa el patrón de bucle dividido.

  • El bucle exterior del ejemplo anterior se ha dividido en dos. (El segundo bucle tiene un bucle interno, que se espera porque el código está iterando en un conjunto de trabajos (o marcadores de posición) y dentro de ese conjunto itera en los intervalos coincidentes).
  • Hay un context.sync después de cada bucle principal, pero no context.sync dentro de ningún bucle.
  • El segundo bucle principal recorre en iteración una matriz que se crea en el primer bucle.

Pero la matriz creada en el primer bucle no contiene solo un objeto de Office, como hizo el primer bucle en la sección Lectura de valores del documento con el patrón de bucle dividido. Esto se debe a que parte de la información necesaria para procesar los objetos Range de Word no está en los propios objetos Range, sino que procede de la jobMapping matriz.

Por lo tanto, los objetos de la matriz creada en el primer bucle son objetos personalizados que tienen dos propiedades. La primera es una matriz de intervalos de Word que coinciden con un título de trabajo específico (es decir, una cadena de marcador de posición) y el segundo es una cadena que proporciona el nombre de la persona asignada al trabajo. Esto facilita la escritura y la lectura del bucle final, ya que toda la información necesaria para procesar un intervalo determinado está contenida en el mismo objeto personalizado que contiene el intervalo. El nombre que debe reemplazar correlatedObject.rangesMatchingJob.items[j] es la otra propiedad del mismo objeto: correlatedObject.personAssignedToJob.

Llamamos a esta variación del patrón de bucle dividido el patrón de objetos correlacionados . La idea general es que el primer bucle crea una matriz de objetos personalizados. Cada objeto tiene una propiedad cuyo valor es uno de los elementos de un objeto de colección de Office (o una matriz de dichos elementos). El objeto personalizado tiene otras propiedades, cada una de las cuales proporciona información necesaria para procesar los objetos de Office en el bucle final. Consulte la sección Otros ejemplos de estos patrones para ver un vínculo a un ejemplo en el que el objeto de correlación personalizado tiene más de dos propiedades.

Otra advertencia: a veces se necesita más de un bucle solo para crear la matriz de objetos de correlación personalizados. Esto puede ocurrir si necesita leer una propiedad de cada miembro de un objeto de colección de Office solo para recopilar información que se usará para procesar otro objeto de colección. (Por ejemplo, el código debe leer los títulos de todas las columnas de una tabla de Excel porque el complemento va a aplicar un formato de número a las celdas de algunas columnas basadas en el título de esa columna). Pero siempre puede mantener las context.syncs entre los bucles, en lugar de en un bucle. Consulte la sección Otros ejemplos de estos patrones para obtener un ejemplo.

Otros ejemplos de estos patrones

¿Cuándo no debe usar los patrones de este artículo?

Excel no puede leer más de 5 MB de datos en una llamada determinada de context.sync. Si se supera este límite, se produce un error. (Vea la sección "Complementos de Excel" de Límites de recursos y optimización de rendimiento para complementos de Office para obtener más información). Es muy raro que se enfoque este límite, pero si existe la posibilidad de que esto suceda con el complemento, el código no debe cargar todos los datos en un único bucle y seguir el bucle con .context.sync Pero debe evitar tener un context.sync elemento en cada iteración de un bucle sobre un objeto de colección. En su lugar, defina subconjuntos de los elementos de la colección y recorra en bucle cada subconjunto a su vez, con un context.sync valor entre los bucles. Esto se puede estructurar con un bucle externo que recorre en iteración los subconjuntos y contiene en context.sync cada una de estas iteraciones externas.