React实现多行打字效果如何设置行延迟等待上一行输入完成
问题根因
现有实现出现多行同时打字的核心原因是:遍历文本数组调用createTypingEffect时,所有行的setTimeout计时是并行启动的,每行的字符延迟都是从0开始按100 * 字符索引计算,没有等待上一行打字流程结束就启动了下一行的定时器,符合setTimeout的事件循环执行逻辑。
另外沙盒环境出现字符重复的问题,是因为原代码在setTimeout回调外部提前读取了arrayCopy副本,拿到的是定时器注册时的旧状态,和后续setState的异步更新结果产生冲突。
实现思路
要实现逐行顺序打字效果,只需要把原来并行的定时器逻辑改成串行执行即可:
- 把单行打字逻辑封装成Promise,在当前行所有字符打印完成后触发resolve
- 用async/await按顺序逐行调用打字方法,等上一行Promise resolve之后,再启动下一行的打字流程
- 所有状态更新统一使用函数式
setState读取最新的组件状态,避免闭包缓存旧状态导致的字符异常
修正后完整代码
import React from 'react' import TerminalCursor from 'icons/TerminalCursor' import TerminalPrompt from 'icons/TerminalPrompt' import { v4 as uuidv4 } from 'uuid' type MyProps = {} type MyState = { introArray: string[] } class Terminal extends React.Component<MyProps, MyState> { state: MyState = { introArray: [], } /** * 单行打字方法 * @param text 要打印的行文本 * @param lineIndex 行在数组中的索引 * @param typeSpeed 单字打印间隔,单位ms */ typeSingleLine = (text: string, lineIndex: number, typeSpeed = 100): Promise<void> => { return new Promise((resolve) => { // 先初始化当前行为空字符串 this.setState( (state) => ({ introArray: [...state.introArray.slice(0, lineIndex), '', ...state.introArray.slice(lineIndex + 1)], }), () => { // 逐字追加字符 for (let charIndex = 0; charIndex < text.length; charIndex++) { setTimeout(() => { this.setState((state) => { const currentLineContent = state.introArray[lineIndex] return { introArray: [ ...state.introArray.slice(0, lineIndex), currentLineContent + text[charIndex], ...state.introArray.slice(lineIndex + 1), ], } }, () => { // 最后一个字符打印完成后,结束当前Promise if (charIndex === text.length - 1) resolve() }) }, typeSpeed * charIndex) } } ) }) } async componentDidMount() { // 后续可替换为从sanity拉取的文本数据 const starterArray = [ 'The very first line that needs to finish typing before going to the next line', 'The second line that needs to start being typed after the first line', ] // 按顺序逐行打印,等待上一行完成后再启动下一行 for (let lineIndex = 0; lineIndex < starterArray.length; lineIndex++) { await this.typeSingleLine(starterArray[lineIndex], lineIndex) // 如果需要行与行之间加停顿,可以在这里加延迟,比如 await new Promise(r => setTimeout(r, 300)) } } render() { return ( <div className="w-1/2 h-1/2 p-5 flex items-start justify-start bg-clip-padding bg-slate-900 backdrop-filter backdrop-blur-xl bg-opacity-60 border border-gray-900 rounded"> <div className="flex flex-col"> {this.state.introArray.map((introLine) => ( <div className="flex items-center" key={uuidv4()}> <TerminalPrompt /> <p className="text-white">{introLine}</p> </div> ))} {/* 底部常驻输入光标 */} <div className="flex"> <TerminalPrompt /> <TerminalCursor /> </div> </div> </div> ) } } export default Terminal
扩展说明
- 这种串行实现的可维护性更高,如果后续需要调整打字速度、增加行间隔停顿、支持中途打断打字效果,只需要修改
typeSingleLine方法或者循环里的逻辑即可,不需要手动计算每行动态延迟 - 如果需要支持删除、光标跳转动画,也可以在这个Promise封装的基础上扩展能力,不需要重构整体逻辑
内容的提问来源于stack exchange,提问作者jutJames
相关产品推荐
相关产品推荐

