避免在循环中使用 context.sync

注意

本文假设你已超出使用四个应用程序特定的 Office JavaScript API 中的至少一个(适用于 Excel、Word、OneNote 和 Visio)的起始阶段,这些 API 使用批处理系统与 Office 文档进行交互。 具体而言,你应该知道 的 context.sync 调用的作用,并且应该知道集合对象是什么。 如果尚未处于该阶段,请从 了解 Office JavaScript API 以及本文中“特定于应用程序”下链接到的文档开始。

对于使用 Excel、Word、PowerPoint、OneNote 和 Visio () 的应用程序特定 API 模型之一的 Office 外接程序中的某些编程方案,代码需要从集合对象的每个成员读取、写入或处理某些属性。 例如,需要获取特定表格列中每个单元格的值的 Excel 外接程序,或需要突出显示文档中字符串的每个实例的Word加载项。 需要循环访问集合对象的 属性中的 items 成员;但出于性能原因,需要避免在循环的每次迭代中调用 context.sync 。 每次调用 context.sync 都是从加载项到 Office 文档的往返。 重复往返会损害性能,尤其是在加载项在 Office web 版 中运行时,因为往返会通过 Internet。

注意

本文中的所有示例都使用 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

写入文档

在最简单的情况下,你只写入集合对象的成员,而不是读取其属性。 例如,以下代码在Word文档中以黄色突出显示每个“the”实例。

注意

通常,最好在应用程序run函数的结束“}”字符之前放置一个 final context.sync 字符, (例如 Excel.runWord.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 秒时间在一个包含 Windows Word 中的 200 个实例的文档中完成。 但是,当循环内部的 await context.sync(); 线被注释掉并且同一行刚刚在循环被取消注释时,操作只花费了 1/10 秒。 在 web Word (,使用 Edge 作为浏览器) ,在循环内同步花费了整整 3 秒,循环后的同步仅花费了 6/10 秒,大约要快 5 倍。 在包含 2000 个“the”实例的文档中, (web 上的Word) 80 秒,循环内同步仅用 4 秒,循环后的同步速度大约快 20 倍。

注意

值得一提的是,如果同步同时运行同步,同步内部循环版本是否执行得更快,只需从 前面context.sync()删除await关键字 (keyword) 即可完成。 这将导致运行时启动同步,然后立即启动循环的下一次迭代,而无需等待同步完成。 但是,由于这些原因,这并不像将 完全移 context.sync 出循环那么好。

  • 正如同步批处理作业中的命令已排队一样,批处理作业本身在 Office 中排队,但 Office 支持队列中不超过 50 个批处理作业。 任何更多都会触发错误。 因此,如果循环中的迭代数超过 50 次,则有可能超出队列大小。 迭代次数越大,发生此情况的可能性越大。
  • “Concurrently”并不意味着同时。 执行多个同步操作所需的时间仍比执行一个同步操作长。
  • 并发操作不一定按启动顺序完成。 在前面的示例中,突出显示单词“the”的顺序并不重要,但在某些情况下,必须按顺序处理集合中的项。

使用拆分循环模式从文档中读取值

context.sync当代码在处理每个集合项时必须读取集合项的属性时,避免在循环中避免使用 会更具挑战性。 假设代码需要循环访问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 数组。
  • 第二个循环循环遍历第一个循环创建的数组,并记录 text 每个 paragraph 项的 。 这是可能的, 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}”。 加载项将每个占位符替换为某人的姓名。 加载项的 UI 对本文并不重要。 例如,它可以有一个任务窗格,其中包含三个文本框,每个文本框都标有一个占位符。 用户在每个文本框中输入一个名称,然后按“ 替换” 按钮。 按钮的处理程序创建一个数组,该数组将名称映射到占位符,然后将每个占位符替换为分配的名称。

你无需实际使用此 UI 生成加载项来试验代码。 可以使用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 对象,就像第一个循环 使用拆分循环模式从文档读取值部分中所做的那样。 这是因为处理 Word Range 对象所需的某些信息不在 Range 对象本身中,而是来自 jobMapping 数组。

因此,在第一个循环中创建的数组中的对象是具有两个属性的自定义对象。 第一个是与特定职务匹配的Word范围数组 (即占位符字符串) 第二个是提供分配给该作业的人员姓名的字符串。 这使得最终循环易于编写且易于阅读,因为处理给定区域所需的所有信息都包含在包含该区域的同一自定义对象中。 应替换 correlatedObject.rangesMatchingJob.items[j] 的名称是同一对象的另一个属性:correlatedObject.personAssignedToJob

我们将拆分循环模式的这种变体称为 相关对象 模式。 一般的想法是,第一个循环创建自定义对象的数组。 每个对象都有一个属性,其值为 Office 集合对象 (中的项目之一或) 此类项的数组。 自定义对象具有其他属性,每个属性都提供在最终循环中处理 Office 对象所需的信息。 有关自定义关联对象具有两个以上属性的示例的链接,请参阅 这些模式的其他示例 部分。

还有一个警告:有时只需创建自定义关联对象的数组,就需要多个循环。 如果需要读取一个 Office 集合对象的每个成员的属性,只是为了收集将用于处理另一个集合对象的信息,则可能会发生这种情况。 (例如,您的代码需要读取 Excel 表格中所有列的标题,因为外接程序将基于该列的 title 向某些列的单元格应用数字格式。) 但始终可以在循环之间保留 context.sync,而不是在循环中。 有关示例,请参阅 这些模式的其他示例 部分。

这些模式的其他示例

何时 不应 使用本文中的模式?

在给定的 调用中,Excel 无法读取超过 5 MB 的数据 context.sync。 如果超出此限制,则会引发错误。 (有关详细信息,请参阅 Office 外接程序的资源限制和性能优化的 “Excel 加载项部分”。) 很少达到此限制,但如果加载项有可能发生这种情况,则代码 不应 加载单个循环中的所有数据,并使用 遵循循环 context.sync。 但是,你仍应避免 context.sync 在循环对集合对象的每次迭代中使用 。 相反,定义集合中项的子集,并依次循环每个子集,并在 循环之间循环 context.sync 。 可以使用一个外部循环来构造此内容,该循环循环循环会循环访问子集,并在 context.sync 其中每个外部迭代中包含 。