You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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频繁触发销毁重建。


问题根源

  1. editTheText的状态重置问题
    每次输入文本时,你都会基于原始的textObject重新创建整个textArray,完全丢弃了之前已经加载好的字体数据。比如某一行已经通过setFont加载了font对象,但新生成的textArray[i] = { ...textObject, text: line }直接把font和fontFamily重置回初始的false,相当于之前的字体加载白做了。

  2. 组件频繁销毁的原因
    editTheText调用props.setLineNum(textArray.length)会触发App中lineNum的更新,进而重新生成fontSelect组件列表。如果生成fontSelect时没有给组件设置稳定的key,React会直接销毁旧的所有FontSelect组件,再重新创建新的——这就导致旧组件被销毁,新组件挂载后又要重新调用setFont加载字体,同时结合上面的状态重置,字体数据直接丢失。

  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 20:45:13