最简单可靠的方式是用 settimeout 递归调用模拟流式打字,避免 for 循环中批量设定时器;需清空目标元素、用 textcontent 而非 innerhtml、动态延迟时间,并在真实流式接口中改用 response.body.getreader() 处理分块数据。

用 setTimeout 模拟流式打字最简单可靠
浏览器原生不支持“AI回复逐字吐出”这种效果,得靠 JS 主动控制节奏。直接用 setTimeout 递归调用是最轻量、兼容性最好、也最容易调试的方式——不用引入框架,不依赖 Stream API,连 IE11 都能跑。
常见错误是写成 for 循环里套 setTimeout,结果所有字几乎同时出现。这是因为循环执行极快,所有定时器都设在了同一毫秒起点。
- 正确做法:每次只处理一个字符,把下一次调用写在当前回调里
- 延迟时间别硬写
50,建议用Math.random() * 20 + 30模拟人类停顿感 - 注意清空目标元素再开始,否则会叠加重复内容
function typeText(el, text, i = 0) {
if (i >= text.length) return;
el.textContent = text.slice(0, i + 1);
setTimeout(() => typeText(el, text, i + 1), Math.random() * 20 + 30);
}
// 调用:typeText(document.getElementById('output'), '你好,我在思考中...');
真实 AI 接口返回时要用 response.body.getReader()
如果后端真走 SSE 或 text/event-stream,或者你用 fetch 请求启用了 streaming,那必须用 ReadableStream API 才能拿到分块数据。这时候 setTimeout 就不管用了——数据不是你控制节奏,而是服务端推多少、你收多少。
容易踩的坑是直接对 response.text() 调用流式函数,这会等整个响应结束才开始解析,完全失去“流式”意义。
- 必须用
response.body.getReader()获取读取器 - 每次
reader.read()返回的是{ done, value },value是Uint8Array,需转字符串:new TextDecoder().decode(chunk) - 别忘了处理
done === true的终止逻辑,否则函数不会退出
中文标点和 emoji 会导致光标错位,得用 textContent 而非 innerHTML
用 innerHTML 拼接时,像 。!?… 或 ??这类符号在某些字体下渲染宽度不一,加上浏览器默认的光标闪烁逻辑,会出现“字还没打完,光标已经跳到下一行开头”的错觉。这不是动画问题,是 DOM 渲染与文本度量不一致导致的视觉偏差。
解决方法非常直接:全程只操作 textContent,彻底绕过 HTML 解析。哪怕内容里有 <br> 或 <strong></strong>,也先转义(如用 textContent.replace(/&/g, '&').replace(/, '),等全部输出完成后再用 <code>innerHTML 一次性渲染格式化版本。
- 流式阶段:一律用
el.textContent = str - 避免在打字中途插入
<span></span>或样式类,会触发重排打断节奏 - 如果必须高亮关键词,等流式结束后再做 DOM 替换
移动端 Safari 对 setTimeout 小于 4ms 的值会自动拉长
iOS 上,小于 4ms 的 setTimeout 延迟会被强制提升到约 4–5ms,导致打字节奏变慢、卡顿感明显。这不是 bug,是 Safari 为省电做的主动限制。如果你设了 10ms 间隔,在 iPhone 上实际可能是 14ms;设 2ms,就真变成 4ms。
更隐蔽的问题是:当用户切到后台再切回来,Safari 会批量合并挂起的定时器,造成“突然刷出一大段文字”。所以不能只依赖时间间隔,还得加状态兜底。
- 用
performance.now()记录上一次输出时间,动态调整下次 delay,保证平均节奏稳定 - 监听
visibilitychange事件,页面隐藏时暂停打字,显示时从断点继续 - 避免在
input或textarea上直接做流式输出——iOS 键盘行为会干扰 focus 和光标位置
流式输出看着简单,但中文、移动端、真实流接口三者一叠加,节奏控制、字符边界、渲染一致性全得手动对齐。漏掉任意一个,用户就会觉得“AI 卡了”或者“回答不连贯”。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











