后台任务 API - 提供了由用户代理自动执行的队列任务的能力

后台任务的合作调度 API(也称为后台任务 API 或 requestIdleCallback() API),提供了在确定有空闲时间时,由用户代理自动执行的队列任务的能力。

注意: 此 API 在 Web Worker不可用

概念和用法

Web 浏览器的主线程以其事件循环为中心。该代码绘制要显示的 Document 的任何待处理更新,运行页面需要运行的任何 JavaScript 代码,接受来自输入设备的事件,并将这些事件派遣到应该接收这些事件的元素。此外,事件循环处理与操作系统的交互,更新到浏览器自己的用户界面,等等。这是一个非常繁忙的代码块,并且您的主要 JavaScript 代码可能会在此线程中运行。当然,能够对 DOM 进行修改的大部分甚至全部代码都是在主线程中运行的,因为用户界面的修改通常只对主线程有效。

由于事件处理和屏幕更新是用户注意到性能问题的两种最明显的方式,因此,你的代码必须成为 Web 的好公民,并帮助防止事件循环执行中的停滞。在过去,除了通过编写尽可能高效的代码,以及通过将尽可能多的工作分发给工作线程之外,没有其他可靠的方法来实现这一点。Window.requestIdleCallback() 使得我们可以积极参与帮助确保浏览器的事件循环顺利运行,通过允许浏览器告诉你的代码可以安全地使用多少时间而不会导致系统滞后。如果你在给定的限制范围内处理代码,你可以让用户的体验变得更好。

充分利用空闲的回调

因为空闲回调的目的是给你的代码提供一种与事件循环合作的方式,以确保系统的潜力得到充分的利用,而不至于使其任务过重,导致滞后或其他性能问题,所以你应该深思熟虑如何去使用它们。

  • 对优先级不高的任务使用空闲回调。 因为你不知道已经建立了多少个回调,你也不知道用户的系统有多忙,你不知道你的回调会多长时间运行一次(除非你指定了 timeout)。不能保证每一次通过事件循环(甚至每一个屏幕更新周期)都包括任何空闲的回调被执行;如果事件循环使用了所有可用的时间,你就倒霉了(同样,除非你指定了 timeout)。

  • 闲置回调应尽量不要超过分配的时间。 虽然如果你超过了指定的时间限制,浏览器、你的代码以及一般的 Web 服务都会继续正常运行(即使你远远超过了时间限制),但时间限制的目的是为了确保你给系统留下足够的时间来完成当前通过事件循环的过程,并进入下一个事件循环,而不会导致其他代码停顿或动画效果滞后。目前,timeRemaining() 的上限是 50 毫秒,但在现实中,你的时间往往会少于这个数字,因为在复杂的网站上,事件循环可能已经在蚕食这个时间,而且浏览器扩展需要处理器时间等等。

  • 避免在你的空闲回调中对 DOM 进行修改。 当你的回调运行时,当前帧已经完成了绘制,并且完成了所有布局更新和计算。如果你做了影响布局的改变,你可能会迫使浏览器不得不停止,并进行重新计算。如果你的回调需要改变 DOM,应该使用 Window.requestAnimationFrame() 来调度。

  • 避免无法预测运行时间的任务。 你的空闲回调应该避免做任何可能会花费不可预测时间的事情。例如,任何可能影响布局的事情都应该避免。你也应该避免解析或拒绝 Promise,因为这将在你的回调返回后立即调用该承诺的解析或拒绝处理程序。

  • 在需要的时候使用超时,但只能在需要的时候使用。 使用超时可以确保您的代码及时运行,但它也允许您在没有足够的时间运行时强制浏览器调用您,从而导致延迟或动画停顿。

回退到 setTimeout

由于背景任务 API 是相当新的接口,因此您的代码可能需要能够在尚未支持它的浏览器上工作。你可以通过一个简单的 shim 来实现,它使用 setTimeout() 作为后备选项。这不是一个 polyfill,因为它的功能并不完全相同;setTimeout() 不会让您利用空闲期,而是尽可能地运行你的代码,让我们尽最大努力避免造成用户的性能滞后。

window.requestIdleCallback = window.requestIdleCallback || function(handler) {
  let startTime = Date.now();

 return setTimeout(function() {
    handler({
      didTimeout: false,
      timeRemaining: function() {
        return Math.max(0, 50.0 - (Date.now() - startTime));
     }
    });
  }, 1);
}

如果 window.requestIdleCallback 未定义,我们在此创建它。该函数会记录调用的时间。我们将使用它来计算 timeRemaining() 的返回值。

然后我们调用 setTimeout(),将一个函数传递给它,传递到我们的 requestIdleCallback() 的实现。回调被传递了一个匹配 IdleDeadline 的对象,其中 didTimeout 设置为 false,还有一个 timeRemaining() 方法,该方法被实现为给出回调 50 毫秒的时间。每次调用 timeRemaining() 时,都会从原来的 50 毫秒中减去经过的时间,以确定剩余的时间。

因此,虽然我们的 shim 不像真正的 requestIdleCallback() 那样约束自己在当前事件循环传递中剩余的空闲时间,但它至少限制了回调在每个传递中不超过 50 毫秒的运行时间。

我们为 cancelidlecallback() 的实现更简单:

window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
  clearTimeout(id);
}

如果 cancelIdleCallback() 未定义,则会创建一个将指定的回调 ID 传递给 cleartimeout()

现在,即使在不支持后台任务 API 的浏览器上,您的代码也会工作,尽管没有那么高效。

接口

后台任务 API 仅添加一个新接口:

IdleDeadline

该类型的对象被传递给空闲回调,以提供空闲周期预计会持续多久的估计值,以及回调是否因为超时期已过而正在运行。

该 API 为 Window 增加了 requestIdleCallback()cancelIdleCallback() 方法。

实例

在这个例子中,我们将看看如何使用 requestIdleCallback() 在浏览器闲置时运行耗时的低优先级任务。此外,这个例子还演示了如何使用 requestAnimationFrame() 来安排文档内容的更新。

下面你会发现这个例子只有 HTML 和 JavaScript。没有显示 CSS,因为它对理解这个功能不是特别重要。

HTML 内容

为了对我们要实现的目标有所了解,我们来看看 HTML。它建立了一个框(ID 为 "Container"),用来展示操作的进度(因为你永远不知道解码 "量子丝速子发射" 要花多长时间),以及第二个主框(ID 为 "logBox"),用来显示文本输出。

<p>
  使用 <code>requestIdleCallback()</code> 方法协同安排的后台任务的演示。
</p>

<div class="container">
  <div class="label">解码量子丝速子发射...</div>
  <progress id="progress" value="0"></progress>
  <div class="button" id="startButton">
    开始
  </div>
  <div class="label counter">
    任务 <span id="currentTaskNumber">0</span> of <span id="totalTaskCount">0</span>
  </div>
</div>

<div class="logBox">
  <div class="logHeader">
    日志
  </div>
  <div id="log">
  </div>
</div>

进度框使用 <progress> 元素来显示进度,同时还有一个标签,以呈现进度的数字信息。此外,还有一个 “开始” 按钮( ID 为 startButton),用户将用于启动数据处理。

CSS 内容

body {
  font-family: "Open Sans", "Lucida Grande", "Arial", sans-serif;
  font-size: 16px;
}

.logBox {
  margin-top: 16px;
  width: 400px;
  height:500px;
  border-radius: 6px;
  border: 1px solid black;
  box-shadow: 4px 4px 2px black;
}

.logHeader {
  margin: 0;
  padding: 0 6px 4px;
  height: 22px;
  background-color: lightblue;
  border-bottom: 1px solid black;
  border-radius: 6px 6px 0 0;
}

#log {
  font: 12px "Courier", monospace;
  padding: 6px;
  overflow: auto;
  overflow-y: scroll;
  width: 388px;
  height: 460px;
}

.container {
  width: 400px;
  padding: 6px;
  border-radius: 6px;
  border: 1px solid black;
  box-shadow: 4px 4px 2px black;
  display: block;
  overflow: auto;
}

.label {
  display: inline-block;
}

.counter {
  text-align: right;
  padding-top: 4px;
  float: right;
}

.button {
  padding-top: 2px;
  padding-bottom: 4px;
  width: 100px;
  display: inline-block;
  float: left;
  border: 1px solid black;
  cursor: pointer;
  text-align: center;
  margin-top: 0;
  color: white;
  background-color: darkgreen;
}

#progress {
  width: 100%;
  padding-top: 6px;
}

JavaScript 内容

既然文档结构已经定义好了,那就来构造做工作的 JavaScript 代码吧。目标是:能够将调用函数的请求添加到队列中,并且只要系统空闲,运行这些函数即可获得结果。

变量声明

let taskList = [];
let totalTaskCount = 0;
let currentTaskNumber = 0;
let taskHandle = null;

这些变量用于管理正在等待执行的任务列表,以及有关任务队列的状态信息及其执行:

  • taskList 是一个包含对象的 Array,每个对象表示一项等待运行的任务。
  • totalTaskCount 是一个已经添加到队列中的任务数量的计数器;它只会增加,不会减少。我们用它来计算,以总工作量的百分比来表示进度。
  • currentTaskNumber 用于跟踪到目前为止已经处理了多少任务。
  • taskHandle 是对当前正在处理的任务的引用。
let totalTaskCountElem = document.getElementById("totalTaskCount");
let currentTaskNumberElem = document.getElementById("currentTaskNumber");
let progressBarElem = document.getElementById("progress");
let startButtonElem = document.getElementById("startButton");
let logElem = document.getElementById("log");

接下来我们有一些变量,这些变量引用了我们需要与之交互的 DOM 元素。这些元素是

  • totalTaskCountElem 是我们用来在进度框的状态显示中插入创建的任务总数的 <span> 元素。
  • currentTaskNumberElem 是用来显示目前已处理的任务数量的元素。
  • progressBarElem 是一个 <progress> 元素,用于显示到目前为止已处理任务的百分比。
  • startButtonElem 是启动按钮。
  • logElem 是一个 <div> 元素,我们将插入记录的文本消息。
let logFragment = null;
let statusRefreshScheduled = false;

最后,我们为其他项目设置了几个变量。

  • logFragment 将用来存储一个 DocumentFragment,由我们的日志函数生成,以创建内容,在下一帧动画渲染时追加到日志中。
  • statusRefreshScheduled 用于跟踪我们是否已经计划了即将框架的状态显示框的更新,以便每帧仅执行一次。

如果不支持空闲回调,则使用 shim 来发挥作用。上面已经讨论过了,所以为了节省文章的篇幅,把它隐藏在这里。

window.requestIdleCallback = window.requestIdleCallback || function(handler) {
  let startTime = Date.now();

  return setTimeout(function() {
    handler({
      didTimeout: false,
      timeRemaining: function() {
        return Math.max(0, 50.0 - (Date.now() - startTime));
      }
    });
  }, 1);
};

window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
  clearTimeout(id);
};

管理任务队列

接下来,让我们来看看我们管理需要执行的任务的方式。我们将通过创建 FIFO 队列的任务来完成此操作,我们将在空闲回调期间运行。

入队任务

首先,我们需要一个函数,用于保留将来要执行的任务。该函数为 enqueueTask(),看起来像这样:

function enqueueTask(taskHandler, taskData) {
  taskList.push({
    handler: taskHandler,
    data: taskData
  });

  totalTaskCount++;

  if (!taskHandle) {
    taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
  }

  scheduleStatusRefresh();
}

enqueueTask() 接受输入两个参数:

  • taskHandler 是一个函数,可以调用来处理任务。
  • taskData 是一种对象,它将作为输入参数传递给任务处理程序,以允许任务接收自定义数据。

要对任务入队,我们将一个对象 pushtaskList 数组上;该对象包含 taskHandlertaskData 的值,分别以 handlerdata 的名字命名,然后递增 totalTaskCount,它表示曾经被入队的任务总数(当任务从队列中移除时,我们不会递减它)。

接下来,我们检查是否已经创建了一个空闲回调;如果 taskHandle 为 0,我们知道还没有空闲回调,所以我们调用 requestIdleCallback() 来创建一个。它被配置为调用一个名为 runTaskQueue() 的函数,这个我们稍后会看到。timeout 设置为 1 秒,这样即使没有实际的空闲时间,它也会每秒运行一次。

运行任务

我们的空闲回调处理程序,runTaskQueue(),会在浏览器确定有足够的空闲时间让我们做一些工作,或者在一秒钟的超时时间到期时被调用。这个函数的工作是运行我们已入队的任务。

function runTaskQueue(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskList.length) {
    let task = taskList.shift();
    currentTaskNumber++;

    task.handler(task.data);
    scheduleStatusRefresh();
  }

  if (taskList.length) {
    taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000} );
  } else {
    taskHandle = 0;
  }
}

runTaskQueue() 的核心是一个循环,只要剩下的时间大于 0(通过检查 deadline.timeRemaining),未超过超时限制(deadline.didTimeouttrue),并且任务列表中存在任务,这个循环就会继续。

对于队列中每一个有时间执行的任务,我们做如下操作。

  1. 从队列中移除任务对象
  2. 递增 currentTaskNumber 来跟踪我们已经执行了多少个任务。
  3. 调用任务的处理程序 task.handler,将任务的数据对象 (task.data) 传入其中。
  4. 我们调用函数 scheduleStatusRefresh() 来处理调度屏幕更新,以反映我们进度的变化。

当时间耗尽时,如果列表中还有任务,我们再次调用 requestIdleCallback(),这样我们就可以在下次有空闲时间时继续处理任务。如果队列是空的,我们将 taskHandle 设置为 0,以表示我们没有需要安排的回调。这样,下次调用 enqueueTask() 时,我们就知道要请求回调了。

更新状态显示

我们希望能做的一件事是使用日志输出和进度信息更新我们的文档。但是,您无法在空闲回调中安全地更改 DOM。相反,我们将使用 requestAnimationFrame() 询问浏览器在更新显示时安全地调用我们。

调度显示更新

通过调用scheduleStatusRefresh() 函数来安排 DOM 更改。

function scheduleStatusRefresh() {
    if (!statusRefreshScheduled) {
      requestAnimationFrame(updateDisplay);
      statusRefreshScheduled = true;
  }
}

这是一个简单的函数。它通过检查 statusRefreshScheduled 的值来检查我们是否已经安排了一次显示刷新。如果为 false,我们就调用 requestAnimationFrame() 来安排刷新,提供要调用的 updateDisplay() 函数来处理这项工作。

更新显示

updateDisplay() 函数负责绘制进度框和日志的内容。当 DOM 处于安全条件时,浏览器调用它,以便我们在渲染下一帧的过程中应用更改。

function updateDisplay() {
  let scrolledToEnd = logElem.scrollHeight - logElem.clientHeight <= logElem.scrollTop + 1;

  if (totalTaskCount) {
    if (progressBarElem.max != totalTaskCount) {
      totalTaskCountElem.textContent = totalTaskCount;
      progressBarElem.max = totalTaskCount;
    }

    if (progressBarElem.value != currentTaskNumber) {
      currentTaskNumberElem.textContent = currentTaskNumber;
      progressBarElem.value = currentTaskNumber;
    }
  }

  if (logFragment) {
    logElem.appendChild(logFragment);
    logFragment = null;
  }

  if (scrolledToEnd) {
      logElem.scrollTop = logElem.scrollHeight - logElem.clientHeight;
  }

  statusRefreshScheduled = false;
}

首先,如果将日志中的文本滚动到底部,则将 scrolledToEnd 设置为 true;否则它被设置为 false。我们将使用它来确定是否应更新滚动位置,以确保在完成向其添加内容后该日志停留在末尾。

接下来,如果有任何任务入队,我们将更新进度和状态信息。

  1. 如果进度条的当前最大值与当前入队任务的总数(totalTaskCount)不同,那么我们就更新显示的任务总数(totalTaskCountElem)和进度条的最大值的内容,使其正常缩放。

  2. 我们对目前处理的任务数做同样的事情,如果 progressBarElem.value 与当前正在处理的任务数(currentTaskNumber)不同,那么我们就更新当前正在处理的任务的显示值和进度条的当前值。

然后,如果有文本等待添加到日志中(也就是说,如果 logFragment 不是 null),我们使用 Element.appendChild() 将其追加到日志元素中,并将 logFragment 设置为 null,这样我们就不会再次添加它。

如果在启动时将日志滚动到末尾,我们要确保它还在。然后我们将 statusRefreshScheduled 设置为 false,表示我们已经处理了刷新,并且可以安全地请求新的刷新。

将文本添加到日志中

log() 函数将指定的文本添加到日志中。由于在调用 log() 的时候,我们并不知道是否可以立即更改 DOM,所以我们会将日志文本缓存起来,直到可以安全更新为止。上面,在 updateDisplay() 的代码中,您可以在更新动画帧时,将日志文本添加到日志元素中的代码。

function log(text) {
  if (!logFragment) {
      logFragment = document.createDocumentFragment();
  }

  const el = document.createElement("div");
  el.textContent = text;
  logFragment.appendChild(el);
}

首先,我们创建名为 logfragmentDocumentFragment 对象(如果不存在的话)。此元素是一个伪 DOM,我们可以在其中插入元素而不立即更改主 DOM 本身。

然后,我们创建一个新的 <div> 元素并设置其内容以匹配输入文本。然后,我们将新元素附加到 logfragment 的伪 DOM 的末尾。logFragment 将累积日志条目,直到下次因为 DOM 改变,调用 updatedisplay() 而被清空。

运行任务

现在,我们已经完成了任务管理和显示维护的代码,我们实际上可以开始设置运行任务的代码,以完成工作。

任务处理程序

我们将作为任务处理程序使用的函数(也就是将用作任务对象 handler 属性的值的函数)是 logTaskHandler()。这是一个简单的函数,它为每个任务向日志输出一堆东西。在你自己的应用程序中,你可以用你希望在空闲时间执行的任何任务来替换这段代码。只要记住,任何你想做的改变 DOM 的事情都需要通过 requestAnimationFrame() 来处理。

function logTaskHandler(data) {
  log("<strong>运行任务 #" + currentTaskNumber + "</strong>");

  for (i=0; i<data.count; i+=1) {
    log((i+1).toString() + ". " + data.text);
  }
}

主程序

当用户单击 “开始” 按钮时,所有内容都会触发,从而导致 decodeTechnoStuff() 函数被调用。

getRandomIntInclusive() 方法来自于 Math.random() 的例子;我们只是在下面链接到它,但为了使例子有效,这里需要包含它。

function getRandomIntInclusive(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
function decodeTechnoStuff() {
  totalTaskCount = 0;
  currentTaskNumber = 0;
  updateDisplay();

  let n = getRandomIntInclusive(100, 200);

  for (i=0; i<n; i++) {
    let taskData = {
      count: getRandomIntInclusive(75, 150),
      text: "该文本来自任务编号:" + (i+1).toString() + " " + n
    };

    enqueueTask(logTaskHandler, taskData);
  }
}

document.getElementById("startButton").addEventListener("click", decodeTechnoStuff, false);

decodeTechnoStuff() 首先将 totalTaskCount(目前添加到队列中的任务数量)和 currentTaskNumber(当前正在运行的任务)的值清零,然后调用 updateDisplay() 将显示屏重置为 "什么都没有发生" 的状态。

这个例子将创建一个随机数量的任务(在 100 到 200 个之间)。为此,我们使用 Math.random() 文档中作为示例提供的 getRandomIntInclusive() 函数 来获取要创建的任务数量。

然后我们启动一个循环以创建实际任务。对于每个任务,我们创建一个 taskData 对象,其中包括两个属性:

  • count 是任务要输出到日志中的字符串数量。
  • text 是按 count 指定的次数向日志输出的文本。

然后,通过调用 enqueueTask(),将 logTaskHandler() 作为处理函数,并将 taskData 对象作为调用该函数时要传入的对象,对每个任务进行入队。

结果

以下是上面代码的实际运作结果。尝试一下,在浏览器的开发人员工具中使用它,并在您自己的代码中使用它。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <style type="text/css">
            body {
              padding: 0;
              margin: 0;
            }

            svg:not(:root) {
              display: block;
            }

            .playable-code {
              background-color: #f4f7f8;
              border: none;
              border-left: 6px solid #558abb;
              border-width: medium medium medium 6px;
              color: #4d4e53;
              height: 100px;
              width: 90%;
              padding: 10px 10px 0;
            }

            .playable-canvas {
              border: 1px solid #4d4e53;
              border-radius: 2px;
            }

            .playable-buttons {
              text-align: right;
              width: 90%;
              padding: 5px 10px 5px 26px;
            }
        </style>
        
        <style type="text/css">
            body {
  font-family: "Open Sans", "Lucida Grande", "Arial", sans-serif;
  font-size: 16px;
}

.logBox {
  margin-top: 16px;
  width: 400px;
  height:500px;
  border-radius: 6px;
  border: 1px solid black;
  box-shadow: 4px 4px 2px black;
}

.logHeader {
  margin: 0;
  padding: 0 6px 4px;
  height: 22px;
  background-color: lightblue;
  border-bottom: 1px solid black;
  border-radius: 6px 6px 0 0;
}

#log {
  font: 12px "Courier", monospace;
  padding: 6px;
  overflow: auto;
  overflow-y: scroll;
  width: 388px;
  height: 460px;
}

.container {
  width: 400px;
  padding: 6px;
  border-radius: 6px;
  border: 1px solid black;
  box-shadow: 4px 4px 2px black;
  display: block;
  overflow: auto;
}

.label {
  display: inline-block;
}

.counter {
  text-align: right;
  padding-top: 4px;
  float: right;
}

.button {
  padding-top: 2px;
  padding-bottom: 4px;
  width: 100px;
  display: inline-block;
  float: left;
  border: 1px solid black;
  cursor: pointer;
  text-align: center;
  margin-top: 0;
  color: white;
  background-color: darkgreen;
}

#progress {
  width: 100%;
  padding-top: 6px;
}
        </style>
        
        <title>后台任务 API  - 实例 - 代码示例</title>
    </head>
    <body>
        
<p>
  使用 <code>requestIdleCallback()</code> 方法协同安排的后台任务的演示。
</p>

<div class="container">
  <div class="label">解码量子丝速子发射...</div>
  <progress id="progress" value="0"></progress>
  <div class="button" id="startButton">
    开始
  </div>
  <div class="label counter">
    任务 <span id="currentTaskNumber">0</span> of <span id="totalTaskCount">0</span>
  </div>
</div>

<div class="logBox">
  <div class="logHeader">
    日志
  </div>
  <div id="log">
  </div>
</div>
        
            <script>
                let taskList = [];
let totalTaskCount = 0;
let currentTaskNumber = 0;
let taskHandle = null;

let totalTaskCountElem = document.getElementById("totalTaskCount");
let currentTaskNumberElem = document.getElementById("currentTaskNumber");
let progressBarElem = document.getElementById("progress");
let startButtonElem = document.getElementById("startButton");
let logElem = document.getElementById("log");

let logFragment = null;
let statusRefreshScheduled = false;

window.requestIdleCallback = window.requestIdleCallback || function(handler) {
  let startTime = Date.now();

  return setTimeout(function() {
    handler({
      didTimeout: false,
      timeRemaining: function() {
        return Math.max(0, 50.0 - (Date.now() - startTime));
      }
    });
  }, 1);
};

window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
  clearTimeout(id);
};

function enqueueTask(taskHandler, taskData) {
  taskList.push({
    handler: taskHandler,
    data: taskData
  });

  totalTaskCount++;

  if (!taskHandle) {
    taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
  }

  scheduleStatusRefresh();
}

function runTaskQueue(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskList.length) {
    let task = taskList.shift();
    currentTaskNumber++;

    task.handler(task.data);
    scheduleStatusRefresh();
  }

  if (taskList.length) {
    taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000} );
  } else {
    taskHandle = 0;
  }
}

function scheduleStatusRefresh() {
    if (!statusRefreshScheduled) {
      requestAnimationFrame(updateDisplay);
      statusRefreshScheduled = true;
  }
}

function updateDisplay() {
  let scrolledToEnd = logElem.scrollHeight - logElem.clientHeight <= logElem.scrollTop + 1;

  if (totalTaskCount) {
    if (progressBarElem.max != totalTaskCount) {
      totalTaskCountElem.textContent = totalTaskCount;
      progressBarElem.max = totalTaskCount;
    }

    if (progressBarElem.value != currentTaskNumber) {
      currentTaskNumberElem.textContent = currentTaskNumber;
      progressBarElem.value = currentTaskNumber;
    }
  }

  if (logFragment) {
    logElem.appendChild(logFragment);
    logFragment = null;
  }

  if (scrolledToEnd) {
      logElem.scrollTop = logElem.scrollHeight - logElem.clientHeight;
  }

  statusRefreshScheduled = false;
}
function log(text) {
  if (!logFragment) {
      logFragment = document.createDocumentFragment();
  }

  const el = document.createElement("div");
  el.textContent = text;
  logFragment.appendChild(el);
}

function logTaskHandler(data) {
  log("运行任务 #" + currentTaskNumber);

  for (i=0; i<data.count; i+=1) {
    log((i+1).toString() + ". " + data.text);
  }
}

function getRandomIntInclusive(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function decodeTechnoStuff() {
  totalTaskCount = 0;
  currentTaskNumber = 0;
  updateDisplay();

  let n = getRandomIntInclusive(100, 200);

  for (i=0; i<n; i++) {
    let taskData = {
      count: getRandomIntInclusive(75, 150),
      text: "该文本来自任务编号:" + (i+1).toString() + " / " + n
    };

    enqueueTask(logTaskHandler, taskData);
  }
}

document.getElementById("startButton").addEventListener("click", decodeTechnoStuff, false);
            </script>
    </body>
</html>

尝试一下 »

规范

规范 状态 备注
Cooperative Scheduling of Background Tasks 建议推荐 -

桌面浏览器兼容性

特性ChromeEdgeFirefoxInternet ExplorerOperaSafari
基础支持4779

551

53 — 552

不支持34 不支持

移动浏览器兼容性

特性AndroidChrome for AndroidEdge mobileFirefox for AndroidIE mobileOpera AndroidiOS Safari
基础支持4747 未知

551

53 — 552

未知34 不支持

1. 默认启用。

2. 已实现,但默认情况下处于禁用状态。

相关链接