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 a context.sync
y debe saber qué es un objeto de 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.
Los complementos de Office que usan uno de los modelos de API específicos de la aplicación pueden tener escenarios que requieran que el código lea o escriba alguna propiedad de cada miembro de un objeto de colección. Por ejemplo, un complemento de Excel que obtiene los valores de cada celda de una columna de tabla determinada o un complemento de Word que resalta cada instancia de una cadena del documento. Tendrá que 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
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.sync
totalmente innecesario. Por lo tanto, en este artículo, lo dejamos fuera cuando no hay comandos sin sincronizar al final de run
.
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.
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 completamente el context.sync
bucle fuera del bucle por las siguientes 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
Evitar dentro de context.sync
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++) {
// 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);
}
});
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úncontext.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 uncontext.sync
objeto para rellenar latext
propiedad delparagraph
objeto proxy. En su lugar, agrega elparagraph
objeto a una matriz. - El segundo bucle recorre en iteración la matriz creada por el primer bucle y registra el
text
de cadaparagraph
elemento. Esto es posible porque elcontext.sync
que se produjo entre los dos bucles rellenaba todas lastext
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.
- Reemplace el bucle por dos bucles.
- Cree un primer bucle para recorrer en iteración la colección y agregar cada elemento a una matriz al mismo tiempo que cargue cualquier propiedad del elemento que el código necesita leer.
- Siga el primer bucle con
context.sync
para rellenar los objetos proxy con las propiedades cargadas. - 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. Aunque la interfaz de usuario del complemento no es importante para este artículo, el complemento 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) => {
// 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();
}
}
});
En el código anterior, hay un bucle externo y un bucle interno. Cada uno de ellos contiene una context.sync
llamada. 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 nocontext.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.sync
s 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
- Para obtener un ejemplo muy sencillo para Excel que usa
Array.forEach
bucles, vea la respuesta aceptada a esta pregunta de Stack Overflow: ¿Es posible poner en cola más de un context.load antes de context.sync? - Para obtener un ejemplo sencillo de Word que usa
Array.forEach
bucles y no usaawait
async
/sintaxis, vea la respuesta aceptada a esta pregunta de Stack Overflow: Iteración en todos los párrafos con controles de contenido con la API de JavaScript de Office. - Para obtener un ejemplo de Word que se escribe en TypeScript, vea el ejemplo Word Comprobador de estilos angular2 del complemento, especialmente el archivo word.document.service.ts. Tiene una mezcla de
for
bucles yArray.forEach
. - Para obtener un ejemplo de Word avanzado, importe este gist en la herramienta Script Lab. Para obtener contexto en el uso del gist, vea la respuesta aceptada al documento de pregunta de Stack Overflow que no está sincronizado después de reemplazar el texto. En este ejemplo se crea un tipo de objeto de correlación personalizado que tiene tres propiedades. Usa un total de tres bucles para construir la matriz de objetos correlacionados y dos bucles más para realizar el procesamiento final. Hay una mezcla de
for
bucles yArray.forEach
. - Aunque no es estrictamente un ejemplo del bucle dividido o los patrones de objetos correlacionados, hay un ejemplo avanzado de Excel que muestra cómo convertir un conjunto de valores de celda a otras monedas con un solo
context.sync
. Para probarlo, abra la herramienta Script Lab y, a continuación, busque y vaya al ejemplo convertidor de divisas.
¿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.