简体   繁体   English

事件侦听器中的多个 state 更改,如何不批处理 DOM 更新?

[英]Multiple state changes in event listener, how to NOT batch the DOM updates?

I'm building a component to test the performance of different algorithms.我正在构建一个组件来测试不同算法的性能。 The algorithms return the ms they took to run and this is want I want to display.算法返回它们运行所需的毫秒数,这就是我想要显示的。 The "fastAlgorithm" takes about half a second, and the "slowAlgorithm" takes around 5 seconds. “fastAlgorithm”大约需要半秒,“slowAlgorithm”大约需要 5 秒。

My problem is that the UI is not re-rendered with the result until both algorithms have finished.我的问题是,在两种算法都完成之前,UI 不会使用结果重新呈现。 I would like to display the result for the fast algorithm as soon as it finishes, and the slow algorithm when that one finishes.我想在完成后立即显示快速算法的结果,并在完成时显示慢速算法的结果。

I've read about how React batches updates before re-rendering, but is there someway to change this behavior?我读过 React 如何在重新渲染之前批量更新,但是有什么办法可以改变这种行为吗? Or is there a better way to organize my component/s to achieve what I want?或者有没有更好的方法来组织我的组件来实现我想要的?

I'm using react 16.13.1我正在使用反应 16.13.1

Here is my component:这是我的组件:

import { useState } from 'react'
import { fastAlgorithm, slowAlgorithm } from '../utils/algorithms'

const PerformanceTest = () => {

  const [slowResult, setSlowResult] = useState(false)
  const [fastResult, setFastResult] = useState(false)

  const testPerformance = async () => {
    fastAlgorithm().then(result => {
      setFastResult(result)
    })
    slowAlgorithm().then(result => {
      setSlowResult(result)
    })
  }

  return (
    <div>
      <button onClick={testPerformance}>Run test!</button>
      <div>{fastResult}</div>
      <div>{slowResult}</div>
    </div>
  )
}

export default PerformanceTest

I read somewhere that ReactDOM.flushSync() would trigger the re-rendering on each state change, but it did not make any difference.我在某处读到 ReactDOM.flushSync() 会在每次 state 更改时触发重新渲染,但它没有任何区别。 This is what I tried:这是我试过的:

const testPerformance = async () => {
  ReactDOM.flushSync(() =>
    fastAlgorithm().then(result => {
      setFastResult(result)
    })
  )
  ReactDOM.flushSync(() => 
    slowAlgorithm().then(result => {
      setSlowResult(result)
    })
  )
}

And also this:还有这个:

const testPerformance = async () => {
  fastAlgorithm().then(result => {
    ReactDOM.flushSync(() =>
      setFastResult(result)
    )
  })
  slowAlgorithm().then(result => {
    ReactDOM.flushSync(() =>
      setSlowResult(result)
    )
  })
}

I also tried restructuring the algorithms so they didn't use Promises and tried this, with no luck:我还尝试重组算法,使它们不使用 Promises 并尝试了这个,但没有成功:

 const testPerformance = () => {
   setFastResult(fastAlgorithm())
   setSlowResult(slowAlgorithm())
 }

Edit编辑

As Sujoy Saha suggested in a comment below, I replaced my algorithms with simple ones using setTimeout(), and everything works as expected.正如Sujoy Saha在下面的评论中所建议的那样,我使用 setTimeout() 将我的算法替换为简单的算法,并且一切都按预期进行。 "Fast" is displayed first and then two seconds later "Slow" is displayed.首先显示“Fast”,两秒后显示“Slow”。

However, if I do something like the code below it doesn't work.但是,如果我执行类似下面的代码的操作,它就不起作用。 Both "Fast" and "Slow" shows up when the slower function finishes... Does anyone know exactly when/how the batch rendering in React happens, and how to avoid it?当较慢的 function 完成时,“快速”和“慢速”都会出现......有谁知道 React 中的批处理渲染何时/如何发生,以及如何避免它?

export const slowAlgorithm  = () => {
  return new Promise((resolve, reject) => {
    const array = []
    for(let i = 0; i < 9000; i++) {
      for(let y = 0; y < 9000; y++) {
        array.push(y);
      }
    }
    resolve('slow')
  })
}

Your initial PerfomanceTest component is correct.您的初始PerfomanceTest组件是正确的。 The component will re-render for the each state change.该组件将为每个 state 更改重新呈现。 I think issue is in your algorithm.我认为问题出在您的算法中。 Please let us know how did you returned promise there.请告诉我们您是如何返回 promise 的。 Follow below code snippet for your reference.按照下面的代码片段供您参考。

export const fastAlgorithm  = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('fast')
    }, 1000)
  })
}

export const slowAlgorithm  = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('slow')
    }, 3000)
  })
}

Are you running your algorithms synchronously on the main thread?您是否在主线程上同步运行您的算法? If so, that's probably what's blocking React from re-rendering.如果是这样,那可能就是阻止 React 重新渲染的原因。 You may need to move them to worker threads .您可能需要将它们移动到工作线程

The below is loosely based on this answer , minus all the compatibility stuff (assuming you don't need IE support):以下大致基于此答案,减去所有兼容性内容(假设您不需要 IE 支持):

 // `args` must contain all dependencies for the function. const asyncify = (fn) => { return (...args) => { const workerStr = `const fn = ${fn.toString()} self.onmessage = ({ data: args }) => { self.postMessage(fn(...args)) }` const blob = new Blob([workerStr], { type: 'application/javascript' }) const worker = new Worker(URL.createObjectURL(blob)) let abort = () => {} const promise = new Promise((resolve, reject) => { worker.onmessage = (result) => { resolve(result.data) worker.terminate() } worker.onerror = (err) => { reject(err) worker.terminate() } // In case we need it for cleanup later. // Provide either a default value to resolve to // or an Error object to throw abort = (value) => { if (value instanceof Error) reject(value) else resolve(value) worker.terminate() } }) worker.postMessage(args) return Object.assign(promise, { abort }) } } const multiplySlowly = (x, y) => { const start = Date.now() const arr = [...new Array(x)].fill([...new Array(y)]) return { x, y, result: arr.flat().length, timeElapsed: Date.now() - start, } } const multiplySlowlyAsync = asyncify(multiplySlowly) // rendering not blocked - just pretend this is React const render = (x) => document.write(`<pre>${JSON.stringify(x, null, 4)}</pre>`) multiplySlowlyAsync(999, 9999).then(render) multiplySlowlyAsync(15, 25).then(render)

Note that fn is effectively being eval ed in the context of the worker thread here, so you need to make sure the code is trusted.请注意,这里的fn是在工作线程的上下文中有效地被eval ed,因此您需要确保代码是可信的。 Presumably it is, given that you're already happy to run it on the main thread.大概是这样,因为您已经乐于在主线程上运行它。

For completeness, here's a TypeScript version:为了完整起见,这里有一个 TypeScript 版本:

type AbortFn<T> = (value: T | Error) => void

export type AbortablePromise<T> = Promise<T> & {
    abort: AbortFn<T>
}

// `args` must contain all dependencies for the function.
export const asyncify = <T extends (...args: any[]) => any>(fn: T) => {
    return (...args: Parameters<T>) => {
        const workerStr =
            `const fn = ${fn.toString()}

            self.onmessage = ({ data: args }) => {
                self.postMessage(fn(...args))
            }`

        const blob = new Blob([workerStr], { type: 'application/javascript' })

        const worker = new Worker(URL.createObjectURL(blob))

        let abort = (() => {}) as AbortFn<ReturnType<T>>

        const promise = new Promise<ReturnType<T>>((resolve, reject) => {
            worker.onmessage = (result) => {
                resolve(result.data)
                worker.terminate()
            }

            worker.onerror = (err) => {
                reject(err)
                worker.terminate()
            }

            // In case we need it for cleanup later.
            // Provide either a default value to resolve to
            // or an Error object to throw
            abort = (value: ReturnType<T> | Error) => {
                if (value instanceof Error) reject(value)
                else resolve(value)

                worker.terminate()
            }
        })

        worker.postMessage(args)

        return Object.assign(promise, { abort }) as AbortablePromise<
            ReturnType<T>
        >
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM