共计 2353 个字符,预计需要花费 6 分钟才能阅读完成。
背景痛点
在 Vue 中实现打字机效果时,开发者通常首选 setInterval 方案,但这会带来两个严重问题:

- UI 线程阻塞:当主线程执行 JavaScript 时,浏览器无法处理渲染和用户交互。如果打字内容较长或设备性能较差,会出现明显的卡顿现象。
- 移动端兼容性问题:iOS 设备会主动降低非用户交互触发的定时器优先级(通常限制到 1s/ 次),导致动画完全失效。
通过 Chrome Performance 工具分析可发现,传统方案会导致 Long Task(超过 50ms 的任务)和布局抖动(Layout Thrashing)。
技术方案对比
1. requestAnimationFrame
- 优势:
- 自动匹配屏幕刷新率(通常 60fps)
- 后台标签页自动暂停执行
- 浏览器原生优化队列处理
- 劣势:
- 需要手动控制执行频率
2. Web Animation API
- 优势:
- 声明式动画配置
- 支持硬件加速
- 劣势:
- 文本动画控制粒度较粗
- 兼容性需要 polyfill
3. CSS 动画
- 优势:
- 性能最佳
- 可通过
steps()实现逐字效果 - 劣势:
- 动态内容难以控制
- 无法中途暂停 / 继续
最终选择:requestAnimationFrame + Vue 响应式系统,既保持精细控制又获得最佳性能。
核心实现
1. Composition API 封装
使用 useTyping 组合式函数管理核心逻辑:
interface TypingOptions {
speed?: number // 字 / 分钟
onComplete?: () => void}
export function useTyping(text: Ref<string>, options: TypingOptions = {}) {const displayedText = ref('')
const isPlaying = ref(false)
// 每帧渲染的字符数计算
const charsPerFrame = computed(() => {return Math.max(1, Math.floor(options.speed / (60 * 60)))
})
let rafId: number
let currentIndex = 0
const play = () => {if (isPlaying.value) return
isPlaying.value = true
const totalLength = text.value.length
const frame = () => {if (currentIndex >= totalLength) {options.onComplete?.()
return
}
displayedText.value = text.value.slice(0, currentIndex)
currentIndex += charsPerFrame.value
rafId = requestAnimationFrame(frame)
}
rafId = requestAnimationFrame(frame)
}
const stop = () => {cancelAnimationFrame(rafId)
isPlaying.value = false
}
onUnmounted(stop)
return {displayedText, isPlaying, play, stop}
}
2. 带队列管理的文本流
扩展基础功能支持多段文本连续播放:
class TypingQueue {private queue: string[] = []
private isProcessing = false
constructor(private callback: (text: string) => void) {}
add(text: string) {this.queue.push(text)
if (!this.isProcessing) this.processNext()}
private processNext() {if (!this.queue.length) {
this.isProcessing = false
return
}
this.isProcessing = true
const nextText = this.queue.shift()!
// 实际播放逻辑...
}
}
3. 光标动画实现
使用纯 CSS 实现平滑闪烁效果:
.typing-cursor {
display: inline-block;
width: 0.5em;
height: 1em;
background: currentColor;
animation: blink 1s infinite steps(1);
}
@keyframes blink {50% { opacity: 0}
}
生产环境优化
1. 内存泄漏防护
必须确保组件销毁时取消所有动画帧请求:
onUnmounted(() => {cancelAnimationFrame(rafId)
// 同时清除事件监听器等资源
})
2. 移动端适配策略
针对 Safari 的特殊处理:
// 检测移动设备
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
// 调整帧率
const targetFPS = isMobile ? 30 : 60
const frameInterval = 1000 / targetFPS
避坑指南
- 直接修改 props:
- 错误做法:在子组件中直接修改传入的 text prop
-
正确方案:使用
v-model或 emit 事件 -
异步销毁处理:
- 错误做法:未处理组件卸载时可能仍在执行的动画帧
-
正确方案:在
onUnmounted中清理资源 -
性能监测缺失:
- 错误做法:没有对长文本进行分块渲染
- 正确方案:使用
requestIdleCallback处理超长内容
完整实现可参考:CodeSandbox 示例
通过合理使用浏览器 API 和 Vue 响应式系统,可以实现既流畅又资源高效的类型效果。关键点在于对动画生命周期的严格管理和针对不同设备的动态适配策略。
正文完
