Éviter d’utiliser la méthode context.sync dans des boucles

Remarque

Cet article part du principe que vous avez dépassé le stade initial de l’utilisation d’au moins une des quatre API JavaScript Office spécifiques à une application (pour Excel, Word, OneNote et Visio) qui utilisent un système de traitement par lots pour interagir avec le document Office. En particulier, vous devez savoir ce qu’un appel fait et vous devez savoir ce qu’est un objet de context.sync collection. Si vous n’en êtes pas encore à ce stade, commencez par Comprendre l’API JavaScript Office et la documentation liée à la section « spécifique à l’application » dans cet article.

Les compléments Office qui utilisent l’un des modèles d’API spécifiques à l’application peuvent avoir des scénarios qui nécessitent que votre code lise ou écrive une propriété de chaque membre d’un objet de collection. Par exemple, un complément Excel qui obtient les valeurs de chaque cellule d’une colonne de tableau particulière ou un complément Word qui met en surbrillance chaque instance d’une chaîne dans le document. Vous devez itérer sur les membres dans la propriété de l’objet items de collection ; mais, pour des raisons de performances, vous devez éviter d’appeler context.sync à chaque itération de la boucle. Chaque appel de context.sync est un aller-retour entre le complément et le document Office. Les allers-retours répétés nuisent aux performances, en particulier si le complément s’exécute dans Office sur le Web, car les allers-retours passent par Internet.

Remarque

Tous les exemples de cet article utilisent for des boucles, mais les pratiques décrites s’appliquent à toute instruction de boucle qui peut itérer dans un tableau, notamment les suivantes :

  • for
  • for of
  • while
  • do while

Ils s’appliquent également à toute méthode de tableau à laquelle une fonction est passée et appliquée aux éléments du tableau, notamment les éléments suivants :

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

Remarque

Il est généralement recommandé de placer un caractère final context.sync juste avant le caractère « } » de fermeture de la fonction d’application run (par Excel.runexemple, , Word.run, etc.). C’est parce que la run fonction effectue un appel masqué de context.sync la dernière chose qu’elle fait si, et seulement si, il y a des commandes en file d’attente qui n’ont pas encore été synchronisées. Le fait que cet appel soit masqué peut prêter à confusion, c’est pourquoi nous vous recommandons généralement d’ajouter l’explicite context.sync. Cependant, étant donné que cet article traite de la minimisation des appels de context.sync, il est en fait plus déroutant d’ajouter une finale context.synctotalement inutile . Ainsi, dans cet article, nous laissons de côté lorsqu’il n’y a pas de commandes non synchronisées à la fin du runfichier .

Écriture dans le document

Dans le cas le plus simple, vous écrivez uniquement aux membres d’un objet de collection, vous ne lisez pas leurs propriétés. Par exemple, le code suivant surligne en jaune chaque instance de « the » dans un document 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.");
})

Le code précédent a pris 1 seconde complète dans un document contenant 200 instances de « the » dans Word sur Windows. Mais lorsque la await context.sync(); ligne à l’intérieur de la boucle est commentée et que la même ligne juste après la boucle n’est pas commentée, l’opération n’a pris qu’un 1/10e de seconde. Dans Word sur le web (avec Edge comme navigateur), cela prenait 3 secondes complètes avec la synchronisation à l’intérieur de la boucle et seulement 6/10e de seconde avec la synchronisation après la boucle, environ cinq fois plus rapide. Dans un document avec 2000 instances de « the », il a fallu (dans Word sur le web) 80 secondes avec la synchronisation à l’intérieur de la boucle et seulement 4 secondes avec la synchronisation après la boucle, environ 20 fois plus rapide.

Remarque

Il convient de se demander si la version synchronisation-en-la-boucle s’exécuterait plus rapidement si les synchronisations s’exécutaient simultanément, ce qui pourrait être fait en supprimant simplement le await mot clé du début du context.sync(). Dans ce cas, l’exécution initie la synchronisation, puis démarre immédiatement l’itération suivante de la boucle sans attendre la fin de la synchronisation. Toutefois, ce n’est pas une aussi bonne solution que de sortir context.sync complètement de la boucle pour les raisons suivantes.

  • De même que les commandes d’un traitement par lots de synchronisation sont mises en file d’attente, les tâches sont elles-mêmes mises en file d’attente dans Office, mais Office ne prend pas en charge plus de 50 travaux par lots dans la file d’attente. Plus il y en a, plus il y a de déclencheurs d’erreurs. Par conséquent, s’il y a plus de 50 itérations dans une boucle, il est possible que la taille de la file d’attente soit dépassée. Plus le nombre d’itérations est élevé, plus le risque que cela se produise est élevé.
  • « Simultanément » ne signifie pas simultanément. L’exécution de plusieurs opérations de synchronisation prendrait encore plus de temps que celle d’une seule.
  • Il n’est pas garanti que les opérations simultanées se terminent dans le même ordre que celui dans lequel elles ont commencé. Dans l’exemple précédent, peu importe l’ordre dans lequel le mot « the » est mis en surbrillance, mais il existe des scénarios où il est important que les éléments de la collection soient traités dans l’ordre.

Lire les valeurs du document avec le modèle de boucle fractionnée

Éviter context.sync l’intérieur d’une boucle devient plus difficile lorsque le code doit lire une propriété des éléments de collection au fur et à mesure qu’il traite chacun d’eux. Supposons que votre code doive itérer tous les contrôles de contenu d’un document Word et enregistrer le texte du premier paragraphe associé à chaque contrôle. Votre instinct de programmation peut vous amener à parcourir les contrôles, à charger la text propriété de chaque (premier) paragraphe, context.sync à appeler pour remplir l’objet de paragraphe proxy avec le texte du document, puis à le journaliser. Voici un exemple.

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

Dans ce scénario, pour éviter d’avoir une context.sync dans une boucle, vous devez utiliser un modèle que nous appelons le modèle de boucle fractionnée . Voyons un exemple concret du modèle avant d’en arriver à une description formelle de celui-ci. Voici comment le modèle de boucle de fractionnement peut être appliqué à l’extrait de code précédent. Notez ce qui suit à propos de ce code.

  • Il y a maintenant deux boucles et le context.sync vient entre elles, il n’y a donc pas context.sync d’intérieur dans l’une ou l’autre boucle.
  • La première boucle parcourt les éléments de l’objet de collection et charge la text propriété, tout comme la boucle d’origine, mais la première boucle ne peut pas enregistrer le texte du paragraphe, car elle ne contient plus de a context.sync pour remplir la text propriété de l’objet paragraph proxy. Au lieu de cela, il ajoute l’objet paragraph à un tableau.
  • La deuxième boucle itère à travers le tableau créé par la première boucle et enregistre le text de chaque paragraph élément. Cela est possible, car le context.sync qui est venu entre les deux boucles a rempli toutes les text propriétés.
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);
    }
});

L’exemple précédent suggère la procédure suivante pour transformer une boucle qui contient a context.sync en un modèle de boucle fractionnée.

  1. Remplacez la boucle par deux boucles.
  2. Créez une première boucle pour itérer sur la collection et ajouter chaque élément à un tableau tout en chargeant toute propriété de l’élément que votre code doit lire.
  3. Suivez la première boucle avec context.sync pour remplir les objets proxy avec les propriétés chargées.
  4. Suivez la context.sync avec une deuxième boucle pour itérer sur le tableau créé dans la première boucle et lire les propriétés chargées.

Traiter les objets dans le document avec le modèle d’objets corrélés

Prenons un scénario plus complexe où le traitement des éléments de la collection nécessite des données qui ne figurent pas dans les éléments eux-mêmes. Le scénario envisage un complément Word qui fonctionne sur des documents créés à partir d’un modèle comportant du texte réutilisable. Le texte contient une ou plusieurs instances des chaînes d’espace réservé suivantes : « {Coordinator} », « {Deputy} » et « {Manager} ». Le complément remplace chaque espace réservé par le nom d’une personne. Bien que l’interface utilisateur du complément ne soit pas importante pour cet article, le complément peut avoir un volet Office avec trois zones de texte, chacune étiquetée avec l’un des espaces réservés. L’utilisateur entre un nom dans chaque zone de texte, puis appuie sur un bouton Remplacer . Le gestionnaire du bouton crée un tableau qui mappe les noms aux espaces réservés, puis remplace chaque espace réservé par le nom attribué.

Vous pouvez utiliser l’outil Script Lab pour suivre les extraits de code présentés ici. Dans Word, vous pouvez charger l’exemple « Modèle d’objets corrélés » ou importer cet exemple de code à partir du référentiel GitHub.

L’instruction d’affectation suivante crée le tableau de mappage entre l’espace réservé et les noms attribués.

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

Le code suivant montre comment vous pouvez remplacer chaque espace réservé par le nom qui lui a été attribué si vous utilisez context.sync des boucles intérieures. Cela correspond à la replacePlaceholdersSlow fonction dans l’exemple.

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

Dans le code précédent, il existe une boucle externe et une boucle interne. Chacun d’eux contient un context.sync appel. D’après le premier extrait de code de cet article, vous voyez probablement que le context.sync dans la boucle interne peut simplement être déplacé après la boucle interne. Mais cela laisserait toujours le code avec un context.sync (deux d’entre eux en fait) dans la boucle externe. Le code suivant montre comment vous pouvez supprimer context.sync des boucles. Elle correspond à la replacePlaceholders fonction dans l’exemple. Nous discuterons du code ultérieurement.

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

Notez que le code utilise le modèle de boucle fractionnée.

  • La boucle externe de l’exemple précédent a été divisée en deux. (La deuxième boucle comporte une boucle interne, ce qui est attendu, car le code itération sur un ensemble de tâches (ou d’espaces réservés) et, au sein de cet ensemble, il itére sur les plages correspondantes.)
  • Il y a une context.sync boucle après chaque boucle majeure, mais pas context.sync à l’intérieur d’une boucle.
  • La deuxième boucle majeure itère à travers un tableau créé dans la première boucle.

Toutefois, le tableau créé dans la première boucle ne contient pas uniquement un objet Office comme dans la première boucle dans la section Lecture des valeurs du document avec le modèle de boucle fractionné. Cela est dû au fait qu’une partie des informations nécessaires au traitement des objets de plage Word ne se trouve pas dans les objets de plage eux-mêmes, mais provient du jobMapping tableau.

Ainsi, les objets du tableau créé dans la première boucle sont des objets personnalisés qui ont deux propriétés. La première est un tableau de plages de Word qui correspondent à un titre de poste spécifique (c’est-à-dire une chaîne d’espace réservé) et la seconde est une chaîne qui fournit le nom de la personne affectée à la tâche. Cela rend la boucle finale facile à écrire et à lire, car toutes les informations nécessaires au traitement d’une plage donnée sont contenues dans le même objet personnalisé que celui qui contient la plage. Le nom qui doit remplacer correlatedObject.rangesMatchingJob.items[j] est l’autre propriété du même objet : correlatedObject.personAssignedToJob.

Nous appelons cette variante du modèle de boucle divisée le modèle d’objets corrélés . L’idée générale est que la première boucle crée un tableau d’objets personnalisés. Chaque objet possède une propriété dont la valeur est l’un des éléments d’un objet de collection Office (ou un tableau d’éléments de ce type). L’objet personnalisé possède d’autres propriétés, chacune d’entre elles fournissant les informations nécessaires au traitement des objets Office dans la boucle finale. Voir la section Autres exemples de ces modèles pour un lien vers un exemple où l’objet de corrélation personnalisé a plus de deux propriétés.

Autre mise en garde : il faut parfois plus d’une boucle pour créer le tableau d’objets de corrélation personnalisés. Cela peut se produire si vous devez lire une propriété de chaque membre d’un objet de collection Office uniquement pour collecter des informations qui seront utilisées pour traiter un autre objet de collection. (Par exemple, votre code doit lire les titres de toutes les colonnes d’un tableau Excel, car votre complément va appliquer un format numérique aux cellules de certaines colonnes en fonction du titre de cette colonne.) Mais vous pouvez toujours garder le context.syncs entre les boucles, plutôt que dans une boucle. Voir la section Autres exemples de ces modèles pour un exemple.

Autres exemples de ces modèles

  • Pour obtenir un exemple très simple pour Excel qui utilise Array.forEach des boucles, consultez la réponse acceptée à cette question de Stack Overflow : est-il possible de mettre en file d’attente plusieurs context.load avant context.sync ?
  • Pour obtenir un exemple simple pour Word qui utilise Array.forEach des boucles et n’utiliseawaitasync/pas de syntaxe, consultez la réponse acceptée à cette question de Stack Overflow : Itération sur tous les paragraphes avec des contrôles de contenu avec l’API JavaScript Office.
  • Pour un exemple de Word avancé, importez ce Gist dans l’outil Script Lab. Pour plus de contexte dans l’utilisation de l’essentiel, consultez la réponse acceptée à la question sur le débordement de pile Document non synchronisé après le remplacement du texte. Cet exemple crée un type d’objet de corrélation personnalisé qui possède trois propriétés. Il utilise un total de trois boucles pour construire le tableau d’objets corrélés, et deux boucles supplémentaires pour effectuer le traitement final. Il y a un mélange de boucles et forArray.forEach de .
  • Bien qu’il ne s’agisse pas à proprement parler d’un exemple de boucle de fractionnement ou de modèles d’objets corrélés, il existe un exemple Excel avancé qui montre comment convertir un ensemble de valeurs de cellule en d’autres devises avec un seul context.sync. Pour l’essayer, ouvrez l’outil Script Lab, puis recherchez et accédez à l’exemple Convertisseur de devises.

Quand ne devriez-vous pas utiliser les modèles de cet article ?

Excel sur le Web ne peut pas lire plus de 5 Mo de données dans un appel donné de context.sync. Si cette limite est dépassée, une erreur est générée. (Pour plus d’informations, voir la section « Compléments Excel » de l’article Limites de ressources et optimisation des performances pour les compléments Office .) Il est très rare que cette limite soit approchée, mais s’il y a une chance que cela se produise avec votre complément, votre code ne doit pas charger toutes les données dans une seule boucle et suivre la boucle avec un context.sync. Cependant, vous devez toujours éviter d’avoir une context.sync boucle à chaque itération sur un objet de collection. Au lieu de cela, définissez des sous-ensembles des éléments de la collection et effectuez une boucle sur chaque sous-ensemble à tour de rôle, avec une context.sync boucle intermédiaire. Vous pouvez structurer cela avec une boucle externe qui itère sur les sous-ensembles et contient le context.sync dans chacune de ces itérations externes.