React 18中如何实现每次setState后触发重新渲染?
React 18中Wasm日志实时渲染问题解决方案
问题核心原因
React 18的自动批处理会合并同步触发的状态更新,而Wasm是同步阻塞主线程执行的——在Wasm运行期间,所有setOut调用都会被积压,直到Wasm执行完成后才会一次性触发渲染,导致日志批量显示。你尝试的flushSync无效,是因为主线程被Wasm完全占用,根本没有间隙处理React的渲染任务。
正确解决方案
1. 用微任务触发React渲染(替代setTimeout Hack)
不需要异步函数,而是在每次setOut后加入一个空的微任务,让React在微任务周期中处理渲染,这是比setTimeout更规范、更轻量的方式:
cell.stdOut = (ln: string) => { setOut((prevOut: string) => prevOut === "" ? ln : `${prevOut}\n${ln}`); queueMicrotask(() => {}); };
微任务会在当前同步代码块执行完毕后立即执行,能让React及时处理状态更新,实现逐行渲染的效果,同时不会引入不必要的延迟。
2. 将Wasm移至Web Worker运行(最优解)
如果Wasm的执行不需要直接操作DOM,最彻底的解决方式是把Wasm放到Web Worker中运行:
- 主线程负责UI渲染,Worker负责执行Wasm并通过
postMessage发送日志 - 主线程监听Worker的消息,收到日志时更新状态,此时不会被Wasm阻塞,自然能实时渲染
示例代码:
// 主线程代码 import { useState } from 'react'; export default function WasmRunner() { const [out, setOut] = useState(""); const wasmWorker = new Worker('./wasm-worker.js'); wasmWorker.onmessage = (e) => { if (e.data.type === 'stdout') { setOut(prev => prev ? `${prev}\n${e.data.content}` : e.data.content); } }; const runWasm = () => { wasmWorker.postMessage({ action: 'execute' }); }; return ( <div> <button onClick={runWasm}>执行Wasm</button> <pre>{out}</pre> </div> ); }
// wasm-worker.js // 这里加载并执行Wasm,将stdOut重定向为postMessage import wasmModule from './your-wasm-module.wasm'; async function initWasm() { const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmModule)); // 重定向stdOut instance.exports.stdOut = (ln) => { self.postMessage({ type: 'stdout', content: ln }); }; } initWasm(); self.onmessage = (e) => { if (e.data.action === 'execute') { // 调用Wasm的执行函数 instance.exports.run(); } };
3. 备选:使用useRef结合forceUpdate(不推荐)
如果上述方式都无法实现,可以用useRef保存日志内容,手动触发更新,但这种方式会跳过React的状态优化,可能引发性能问题:
import { useState, useRef } from 'react'; export default function WasmRunner() { const [, setForceUpdate] = useState(0); const outRef = useRef(""); const forceUpdate = () => setForceUpdate(prev => prev + 1); cell.stdOut = (ln: string) => { outRef.current = outRef.current === "" ? ln : `${outRef.current}\n${ln}`; forceUpdate(); }; return ( <div> <button onClick={runWasm}>执行Wasm</button> <pre>{outRef.current}</pre> </div> ); }
关于模拟与实际Wasm的表现
你用循环同步调用stdOut的模拟方式,和实际Wasm运行时的表现完全一致——因为Wasm是同步阻塞执行的,所有stdOut调用都会在同一个同步任务中完成,React会批量处理这些状态更新。只有当Wasm的输出被拆分为不同的任务(比如通过Worker、微任务),才能触发实时渲染。
内容的提问来源于stack exchange,提问作者A. Mair
相关产品推荐
相关产品推荐

