Bootstrap进度条无法显示进度?

3

我在JS中有一个非常长的循环(超过200万次迭代)。 我想向用户展示一些进度。 出于某种原因,代码会卡住,并且只在循环完成后才显示进度。 我做错了什么?

注意:浏览器可能会陷入困境,请设置最大值。

var MAX = 100000;

function doIt(){        
      alert('start count :'+ MAX);
       for(i=0;i<MAX;i++){
          setTimeout(updateBar, 600 , i);
          console.log(i);
       }        
      alert('count ended');
}


function updateBar(idx) {
    var bar = document.getElementById('progress');
     bar.style.width = Math.floor(100 * idx / MAX) + '%';
}
body {padding: 50px;}
<link href="http://getbootstrap.com/2.3.2/assets/css/bootstrap.css" rel="stylesheet"/>

<button onclick="doIt()">Start counting</button>


<div class="progress">
    <div class="bar" id="progress"></div>
</div>


4
JS是单线程的。当它在计算时,不会执行其他任何操作(如更新进度条)。你可以每X次迭代插入超时,或者使用Web Worker,在另一个线程上执行计算。 - blex
2
(或者您可以构建您的计算,使得循环的每次迭代自然地移动该条) - Brightstar
1
很遗憾,这样做行不通。在同步操作期间,浏览器不会重新绘制。在这样的计算过程中,甚至无法让加载GIF动画。 - blex
2
@Brightstar,我觉得我们可能是在相互之间说过话了。确实有可能将进度条传递到函数中,并按照您所描述的进行增量。我的意思是,在JS中,您不能使用经典循环直接更新进度条元素,而是需要使用一些异步方式,如超时/承诺/网页工作者来防止页面的主线程冻结,以便浏览器事件循环有足够的空间呼吸,并将进度条绘制到屏幕上。 - Maximillian Laumeister
1
这其实是OP问题的关键所在。OP没有使用超时来将控制权交给浏览器,因此主线程停留在JS循环中,阻止浏览器事件循环重新绘制屏幕。@blex提供了一个非常好的解决方案,也许是最理想的解决方案,尽管我可能会添加一个更简单的答案,展示将循环迭代分成批处理的老派方法。 - Maximillian Laumeister
显示剩余3条评论
2个回答

2
这是一个经典问题,由于长时间运行的函数没有周期性地停止并将控制权传回到浏览器的事件循环中,因此会导致该问题发生。在此期间,浏览器无法执行其他任务(例如绘制元素):

这种模型的缺点是,如果消息处理时间过长,Web 应用程序将无法处理用户交互,例如单击或滚动。浏览器通过“脚本运行时间过长”对话框来减轻这种情况。遵循的良好实践是使消息处理变得简短,并且如果可能,将一条消息拆分为多条消息。

并发模型和事件循环 - MDN

当浏览器没有任何“空闲时间”可用时,因为您的 JS 循环具有控制权,浏览器无法绘制进度条。
要解决此问题,您需要将循环分成多个块,即像 MDN 建议的那样“将一条消息拆分为多条消息”。为了实现这一点,您需要使用类似超时的异步方式,告诉浏览器在“准备好时”而不是“立即”运行下一次迭代。这样,在函数继续执行之前,浏览器可以执行其他重要的任务(例如处理滚动和单击事件以及绘制元素,如进度条)。

解决方案

以下解决方案通过使用 setTimeout(fn, 0) 在每 x 次迭代时将控制权返回给浏览器来打破循环。

如果您正在进行的任务涉及DOM或因任何其他原因无法在Web Worker中运行,则此方法非常有用。否则,@blex's web worker solution 可能更好,因为任何可以在后台线程中运行的长时间运行的任务都应该这样做,以提高性能。

const MAX = 500000;
let currentIteration = 0;

function doWork(){
       while (currentIteration < MAX) {
        // Do your actual work here, before the increment
        currentIteration++;
        if (currentIteration % 5000 === 0) {
          console.log(currentIteration);
          updateBar(currentIteration);
          setTimeout(doWork, 0);
          return;
        }
       }   
      alert('count ended');
}

function startWork() {
      alert('start count :'+ MAX);
      doWork();
}

function updateBar(idx) {
    var bar = document.getElementById('progress');
     bar.style.width = Math.floor(100 * idx / MAX) + '%';
}
body {padding: 50px;}
<link href="http://getbootstrap.com/2.3.2/assets/css/bootstrap.css" rel="stylesheet"/>

<button onclick="startWork()">Start counting</button>


<div class="progress">
    <div class="bar" id="progress"></div>
</div>


2
你在这个答案上付出了很多努力,我希望提问者能够欣赏它。 - Brightstar
我非常感激! - JavaSheriff

2
以下是可能的解决方案,使用Web Worker

enter image description here

worker-script.js(不同的上下文,不同的文件)

// When this worker receives a message
self.addEventListener("message", onMessageReceive);

function onMessageReceive(e) {
  // Get the max value
  var max = e.data.value;
  // Run the calculation
  doIt(max);
}

function doIt(max) {
  // Send messages to the parent from time to time
  self.postMessage({ type: "start", value: max });
  for (i = 0; i < max; i++) {
    if (i % 5000 === 0) { // Every 5000 iterations - tweak this
      self.postMessage({ type: "progress", value: i });
    }
  }
  self.postMessage({ type: "end", value: max });
}

main.js (in your page)

var MAX = 20000000, // 20 millions, no problem
  worker = null;

// If workers are supported, create one by passing it the script url
if (window.Worker) {
  worker = new Worker("worker-script.js");
  document
    .getElementById("start-btn")
    .addEventListener("click", startWorker);
} else {
  alert("Workers are not supported in your browser.");
}

// To start the worker, send it the max value
function startWorker() {
  worker.postMessage({ value: MAX });
}

// When the worker sends a message back
worker.addEventListener("message", onMessageReceive);

function onMessageReceive(e) {
  var data = e.data;
  switch (data.type) {
    case "start":
      console.log(`start count: ${data.value}`);
      break;
    case "progress":
      updateBar(data.value);
      break;
    case "end":
      updateBar(data.value);
      console.log(`end count: ${data.value}`);
      break;
  }
}

function updateBar(idx) {
  var bar = document.getElementById("progress");
  bar.style.width = Math.floor((100 * idx) / MAX) + "%";
}

index.html

<button id="start-btn">Start counting</button>

<div class="progress">
  <div class="bar" id="progress"></div>
</div>

<script src="main.js"></script>

要明确的是,OP实际执行任务的代码应该放在worker-script.js中的for循环中。 - Maximillian Laumeister

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接