React组件频繁卸载致字体丢失问题求助
我在开发文本转SVG渲染的功能时,碰到了两个棘手问题:字体加载完成后组件突然销毁,已加载的字体直接变回初始的false,导致文本无法正常渲染;同时theText状态频繁被销毁重建。需求是每行文本使用独立字体,我先把文本相关属性整合为一个基础对象:
const textObject = { font: false, // opentype.js字体对象将加载到此处 fontFamily: false, // 此处设置字体族 fontSize: 100, text: "", // 用于生成路径的实际文本字符串 color: "#000000" }
核心代码梳理
App的DOM结构:
return ( <div className="App"> <ContextProvider setLineNum={e => handleLineNum(e)}> {fontSelect} <TextBox /> <TextOutput /> </ContextProvider> </div> );
ContextProvider中拆分文本行的editTheText函数:
const editTheText = (t) => { const textLines = t.split(/\r?\n/); // 若最后一行为空且数组长度大于1,则移除最后一个元素 if (textLines[textLines.length - 1].text === "" && textLines.length > 1) { textLines.splice(-1); }; let textArray = []; textLines.forEach((line, i) => { textArray[i] = { ...textObject, text: line }; setLines(i + 1); }); setTheText(textArray); props.setLineNum(textArray.length); console.log("editTheText", textArray); }
FontSelect组件的字体初始化逻辑:
useEffect(() => { if (fontSelectRef.current) return; // 仅在挂载时运行一次 fontSelectRef.current = true; setFont(initialFont, props.id); // 将字体名称和行号(FontSelect组件的id)发送到Context });
Context中的字体加载函数setFont:
const setFont = (fontFamily, lineNum) => { console.log("setFont", fontFamily, "lineNum", lineNum, theText); if (theText[lineNum].fontFamily !== fontFamily) { console.log("New Font:", fontFamily); opentype.load('ttf/' + fontFamily + '.ttf', (err, font) => { if (err) { alert('Font could not be loaded: ' + err); } else { console.log("loaded Font:", fontFamily); setTheText(prev => { prev[lineNum].font = font; prev[lineNum].fontFamily = fontFamily; return [...prev]; }); } }); } }
监控theText状态的钩子:
useEffect(()=> { console.log('theText effect is running', theText); return () => console.log('theText effect is destroying', theText); }, [theText]);
现在的问题:输入文本时,editTheText运行后,theText里的text被设置为输入内容,但之前加载好的font又变回false,同时theText频繁触发销毁重建。
问题根源
editTheText的状态重置问题
每次输入文本时,你都会基于原始的textObject重新创建整个textArray,完全丢弃了之前已经加载好的字体数据。比如某一行已经通过setFont加载了font对象,但新生成的textArray[i] = { ...textObject, text: line }直接把font和fontFamily重置回初始的false,相当于之前的字体加载白做了。组件频繁销毁的原因
editTheText调用props.setLineNum(textArray.length)会触发App中lineNum的更新,进而重新生成fontSelect组件列表。如果生成fontSelect时没有给组件设置稳定的key,React会直接销毁旧的所有FontSelect组件,再重新创建新的——这就导致旧组件被销毁,新组件挂载后又要重新调用setFont加载字体,同时结合上面的状态重置,字体数据直接丢失。setFont中的状态更新不规范
在setTheText的回调里,你直接修改了prev[lineNum]的属性,虽然最后返回了[...prev],但prev[lineNum]还是原来的对象引用,React可能无法正确识别状态变化,导致更新异常。
解决方案
1. 修改editTheText,保留已加载的字体数据
不要完全重建textArray,而是基于现有的theText状态来更新,只修改需要变动的text字段,保留已有的font和fontFamily:
const editTheText = (t) => { const textLines = t.split(/\r?\n/); // 修正判断条件:textLines里的元素是字符串,不是对象 if (textLines.length > 1 && textLines[textLines.length - 1] === "") { textLines.pop(); } setTheText(prev => { // 生成新数组,复用已有行的字体数据 const newTextArray = textLines.map((line, i) => { // 如果之前有该行的记录,保留font和fontFamily,只更新text if (prev[i]) { return { ...prev[i], text: line }; } // 新行用初始对象 return { ...textObject, text: line }; }); // 移除多余的行(如果之前的行数比现在多) return newTextArray; }); props.setLineNum(textLines.length); }
2. 给FontSelect组件设置稳定的key
在App中生成fontSelect时,用行号作为key,保证组件的复用性,避免行数变化时销毁所有旧组件:
// 假设lineNum是当前的行数 const fontSelect = Array.from({ length: lineNum }, (_, i) => ( <FontSelect key={i} id={i} /> ));
3. 规范setFont中的状态更新
不要直接修改prev数组里的对象,而是创建新的对象来更新,确保React能正确识别状态变化:
const setFont = (fontFamily, lineNum) => { console.log("setFont", fontFamily, "lineNum", lineNum, theText); // 先判断当前行是否存在,避免数组越界 if (!theText[lineNum] || theText[lineNum].fontFamily !== fontFamily) { console.log("New Font:", fontFamily); opentype.load('ttf/' + fontFamily + '.ttf', (err, font) => { if (err) { alert('Font could not be loaded: ' + err); } else { console.log("loaded Font:", fontFamily); setTheText(prev => { // 复制整个数组 const newTextArray = [...prev]; // 复制该行对象,修改字体属性 newTextArray[lineNum] = { ...newTextArray[lineNum], font: font, fontFamily: fontFamily }; return newTextArray; }); } }); } }
内容的提问来源于stack exchange,提问作者user6329530

