【问题标题】:React updating progress from a compute intensive function using setState使用 setState 响应计算密集型函数的更新进度
【发布时间】:2020-02-11 04:58:39
【问题描述】:

我有一个简单的 React 类,显示 this.state.progress(一个数字),这个状态可以通过 updateProgress(progress) 函数更新。

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      progress: 0,
    }
  }

  updateProgress = (progress) => {this.setState({progress}); };
  render() {
    let {progress} = this.state;
    return <h1>{progress}</h1>;
  }
}

我有一个计算密集型函数myHeavyFunc,我需要为此显示进度条。我使用myHeavyFunc 中的循环变量调用上面提到的updateProgress 函数。

myHeavyFunc = async (updateProgress) => {
    let loopLength = 1000000;
    updateProgress(0);
    for(let i=0; i<loopLength; i++) {
      // some processing happens here
      updateProgress((i+1)/loopLength);
    }
}

发生的情况是状态得到更新,我可以通过控制台在setState 回调中记录进度来确认这一点,但组件直到最后才会重新渲染。但是,如果我包含一个 1 毫秒的小 sleep,那么会发生重新渲染,进度更新(显然时间上有巨大的损失,我不喜欢)。

JSFiddle here。在这里,我在单击进度号时运行myHeavyFunc。您可以看到,当 await sleep(1) 被评论时,onClick 在一秒钟内完成,但不显示进度。它甚至不会因任何后续点击而改变。另一方面,如果没有评论,我会得到进度更新,但它只需要永远完成!

我知道 React 出于性能原因应该 batch 更新,但就我而言,在整个循环完成之前我什至看不到一次更新。另外,请注意,我不是在寻找 synchronous setState 函数,但在设置状态后我需要重新渲染(至少仅在进度元素上)。如果它由于批处理而丢失了一些进度更新,我很好,但我确实希望它能够显示进度。

有没有办法在更新 UI 中的进度时以非阻塞方式运行 myHeavyFunc在 React 中更新计算密集型函数进度的正确方法是什么?

【问题讨论】:

  • 更新fiddle
  • 仅仅创建一个函数async 并没有什么神奇的。它让它返回一个 Promise,但是如果它做了一些持续很长时间并且在返回之前是同步的,它仍然会在 React 有机会做任何事情之前运行到完成。
  • 而当您添加 await sleep(1) 时,这会“暂停执行”并允许组件更新
  • 所以简而言之,您使用await sleep(1) 调用的方式是“正确的方式” - 我很欣赏在这种情况下它会使过程太长,在这种情况下只是循环的每 100 步或每 1000 步调用一次。我认为这是基于一个比无操作长循环更现实的示例——在这种情况下,如果你展示你的实际功能,或者至少是一个更现实的近似。
  • @RobinZigmond 我有一个非常相似的设置 - 一个 for 循环,我只是构建一个非常大的数组(~64k json 条目),但是因为这个问题可以推广到我们有一个长时间运行的计算函数,我留下了一个无所事事的循环。

标签: javascript reactjs progress-bar setstate


【解决方案1】:

我建议您尽可能少地通知进度。进度会从 0 到 100,所以我只会通知进度 100 次,并且我会添加一个超时,以便用户可以看到进度转换:

如果循环长度 = 1000000,

1000000 / 100 = 10000

每 10000 次迭代我将调用 updateProgress 函数。

let myHeavyFunc = async (updateProgress) => {
  let loopLength = 1000000;
  let current = 0;
  for(let i=0; i<loopLength; i++) {
    const result = Math.floor(((i+1)/loopLength)*100);
    if(current != result) {
       setTimeout(()=> {
           updateProgress(result);
       }, result *100);
    }
    current = result;
  }
}

见:https://jsfiddle.net/wbzx4j90/1/

【讨论】:

  • 此解决方案有效,并且与 Robin Zigmond 评论中的建议非常相似。为什么超时会随着进度线性增加?
  • 是的,想法是在不同的时间调用updateProgress(result),对于result=1,time=100,result=2,time=200,否则如果你定义相同的时间你会有result=1, time=100, result=2, time = 100,所以用户看不到进度转换。
  • 您只需要始终保持相同的超时时间。超时允许浏览器呈现内容。我正在使用 1ms 来减少开销,小提琴here
  • 我更新了您的 fiddle 以反映相同的数字转换,即 0 到 1,其他所有内容都保持不变。但是你可以看到它比没有超时的情况下花费的时间更多。您可以将其与我在上面发布的小提琴进行比较,以了解时差。
  • 是的,那个时候就是一个例子,你可以少用。您可以尝试 10 或 1,而不是 100
【解决方案2】:

我猜问题在于浏览器重绘。通常,浏览器每秒重绘 60 帧。在您的情况下,您要求浏览器执行更多的绘画。我建议使用 requestAnimationFrame 而不是 for 循环来持续更新 DOM。 requestAnimationFrame 将在浏览器执行下一次重绘之前处理要调用的函数。

  let i = 0
  let loopLength = 1000;

  let myHeavyFunc = (updateProgress) => {
    updateProgress((i+1)/loopLength);
    i++;
  if (i < loopLength) {
    window.requestAnimationFrame(() => myHeavyFunc(updateProgress));
    
  }
}

工作演示https://jsfiddle.net/gdq5ouat/5/

【讨论】:

  • 这太慢了,因为对 UI 的更新对于每次迭代来说都是一个很大的开销,我将无法像问题中提到的那样执行 1000000 循环。
猜你喜欢
  • 1970-01-01
  • 2020-05-09
  • 1970-01-01
  • 1970-01-01
  • 2013-11-26
  • 2012-08-19
  • 2015-05-19
  • 2011-12-07
  • 2017-09-20
相关资源
最近更新 更多