reactjs - 事件侦听器中的多个状态更改,如何不批处理 DOM 更新?
问题描述
我正在构建一个组件来测试不同算法的性能。算法返回他们运行的毫秒数,这是我想要显示的。“fastAlgorithm”大约需要半秒,“slowAlgorithm”大约需要 5 秒。
我的问题是,直到两种算法都完成后,UI 才用结果重新渲染。我想在完成后立即显示快速算法的结果,并在完成后显示慢速算法的结果。
我已经阅读了 React 如何在重新渲染之前批量更新,但是有什么办法可以改变这种行为吗?还是有更好的方法来组织我的组件以实现我想要的?
我正在使用反应 16.13.1
这是我的组件:
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
我在某处读到 ReactDOM.flushSync() 会在每次状态更改时触发重新渲染,但它没有任何区别。这是我尝试过的:
const testPerformance = async () => {
ReactDOM.flushSync(() =>
fastAlgorithm().then(result => {
setFastResult(result)
})
)
ReactDOM.flushSync(() =>
slowAlgorithm().then(result => {
setSlowResult(result)
})
)
}
还有这个:
const testPerformance = async () => {
fastAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setFastResult(result)
)
})
slowAlgorithm().then(result => {
ReactDOM.flushSync(() =>
setSlowResult(result)
)
})
}
我还尝试重组算法,使他们不使用 Promises 并尝试了这个,但没有运气:
const testPerformance = () => {
setFastResult(fastAlgorithm())
setSlowResult(slowAlgorithm())
}
编辑
正如Sujoy Saha在下面的评论中建议的那样,我使用 setTimeout() 将我的算法替换为简单的算法,并且一切都按预期工作。首先显示“Fast”,然后两秒钟后显示“Slow”。
但是,如果我执行以下代码之类的操作,则它不起作用。当较慢的功能完成时,“Fast”和“Slow”都会出现......有谁知道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')
})
}
解决方案
您是否在主线程上同步运行算法?如果是这样,那可能就是阻止 React 重新渲染的原因。您可能需要将它们移至工作线程。
以下内容大致基于此答案,减去所有兼容性内容(假设您不需要 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)
请注意,fn
这里实际上是eval
在工作线程的上下文中编辑的,因此您需要确保代码是可信的。大概是这样,因为您已经乐于在主线程上运行它。
为了完整起见,这是一个 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>
>
}
}
推荐阅读
- jaxb - JAXB Marshaller.Listener 不能与 OutputStream 配合使用
- ember.js - Ember:Application.hbs 在构建时未在正文中生成
- python - 在 Matplotlib 中使用 RGB 值着色
- google-apps-script - Apps 脚本直方图图表生成器设置最大值
- javascript - 从 Flickr api 使用 JSONP 获取 JSON 数据
- sql - Oracle SQL 查询以获取每周记录
- firebase - 如何在cloudfirestore中使用flutter中的父选项?
- javascript - 如何让这个画布动画脚本在 Firefox 中工作?
- postgresql - Amazon RDS + PostgreSQL + 重音 + 喜欢
- r - 在数据框列表上使用 tapply