🧑‍💻 Vue2 打印文字效果实战

48 阅读4分钟

在前端开发中,文字打印动画(Typing Effect) 是一种常见且吸引用户注意力的交互方式。它常用于介绍页、引导语、聊天机器人等场景。

本文实现一个 Vue2 版本的“打字机”效果组件包括:

  • 使用 requestAnimationFrame 实现平滑动画
  • 控制输出速度和字符块大小
  • 生命周期管理与清理
  • 与全局状态通信
  • 扩展功能建议(如暂停/播放、自动滚动等)

🎯 目标

我们要实现一个如下图效果的组件:

这是一段正在逐字显示的文字...|

其中:

  • 文字逐字出现。
  • 末尾有一个闪烁光标 |
  • 支持控制输出速度。
  • 可重复播放。
  • 支持回调通知完成。

🚀 第一步:搭建基础结构

我们先创建一个最简单的 Vue 组件,展示一段文本。

<template>
  <div class="stream-content">
    {{ displayText }}
    <span class="blinking-cursor">|</span>
  </div>
</template>

<script>
export default {
  data() {
    return {
      displayText: ''
    };
  }
};
</script>

这是静态的展示,接下来我们要让它动起来!


🕒 第二步:添加动画逻辑 —— 模拟“打字”过程

我们使用 JavaScript 的 requestAnimationFrame 方法来实现高帧率更新,模拟逐字输入的效果。

✅ 思路拆解:

  1. 定义当前已显示的字符索引 currentIndex
  2. 设置每秒输出字符数 speed 和每次输出字符数量 chunkSize
  3. 计算每一帧的时间间隔。
  4. 在每一帧中更新文本内容。
  5. 当全部输出完毕后触发回调或停止动画。

💡 示例代码片段:

let currentIndex = 0;
const speed = 30; // 字符/秒
const chunkSize = 3;
const interval = 1000 / (speed / chunkSize);

function renderNextChunk(timestamp) {
  const elapsed = timestamp - lastRenderTime;

  if (elapsed >= interval && currentIndex < fullText.length) {
    displayText += fullText.slice(currentIndex, currentIndex + chunkSize);
    currentIndex += chunkSize;
    lastRenderTime = timestamp;
  }

  if (currentIndex < fullText.length) {
    requestAnimationFrame(renderNextChunk);
  }
}

requestAnimationFrame(renderNextChunk);

🧱 第三步:封装为可复用组件(Vue2 版本)

我们将上面的逻辑封装成一个完整的 Vue 组件,支持传参、生命周期管理、以及状态同步。

✨ 最终组件代码(完整版)

下面是最终实现的 StreamDisplay.vue 组件,基于 Vue 2.x 编写。

<template>
  <div class="stream-container">
    <div class="stream-content">
      {{ displayText }}
      <span v-if="isPlaying" class="blinking-cursor">|</span>
    </div>
  </div>
</template>

<script>
export default {
  name: 'StreamDisplay',
  props: {
    fullText: {
      type: String,
      required: true
    },
    speed: {
      type: Number,
      default: 30
    },
    chunkSize: {
      type: Number,
      default: 3
    },
  },
  data() {
    return {
      displayText: '',
      isPlaying: false,
      currentIndex: 0,
      rafId: null,
      lastRenderTime: 0
    };
  },
  computed: {
    interval() {
      return 1000 / (this.speed / this.chunkSize);
    }
  },
  methods: {
    renderNextChunk(timestamp) {
      if (!this.lastRenderTime) this.lastRenderTime = timestamp;

      const elapsed = timestamp - this.lastRenderTime;

      if (elapsed >= this.interval && this.currentIndex < this.fullText.length) {
        const endIndex = Math.min(this.currentIndex + this.chunkSize, this.fullText.length);

        this.displayText += this.fullText.slice(this.currentIndex, endIndex);
        this.currentIndex = endIndex;
        this.lastRenderTime = timestamp;
      }

      if (this.currentIndex < this.fullText.length) {
        this.rafId = requestAnimationFrame(this.renderNextChunk);
      } else {
        this.isPlaying = false;
    
      }
    },
    startStream() {
      if (this.currentIndex >= this.fullText.length) {
        this.currentIndex = 0;
        this.displayText = '';
      }
      this.isPlaying = true;
      this.rafId = requestAnimationFrame(this.renderNextChunk);
    },
    pauseStream() {
      if (this.rafId) {
        cancelAnimationFrame(this.rafId);
        this.rafId = null;
        this.isPlaying = false;
      }
    }
  },
  watch: {
    fullText: {
      immediate: true,
      handler(newVal) {
        if (newVal) {
          this.startStream();
        }
      }
    }
  },
  beforeDestroy() {
    this.pauseStream();
  }
};
</script>

<style scoped>
.stream-container {
  font-family: monospace;
}

.stream-content {
  white-space: pre-wrap;
}

.blinking-cursor {
  display: inline-block;
  width: 1ch;
  animation: blink 1s infinite;
}

@keyframes blink {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}
</style>

🔁 第四步:组件使用示例

你可以像下面这样使用这个组件:

<template>
  <div class="app">
    <StreamDisplay
      :full-text="data"
      :speed="20"
      :chunk-size="1"
    />
  </div>
</template>

<script>
import StreamDisplay from './components/StreamDisplay.vue';

export default {
  components: {
    StreamDisplay
  },
  data() {
    return {
      data: "这是一段测试文本,将会被逐字显示出来..."
    };
  },
  methods: {
    handleSetGlobal(prev) {
      console.log('打印已完成');
      return { ...prev, isStreamFinished: true };
    }
  }
};
</script>

🛠️ 第五步:深度解析关键知识点

1. requestAnimationFrame vs setTimeout

  • requestAnimationFrame 是浏览器提供的动画优化方法,适合做视觉连续更新。
  • 相比 setTimeout,它可以根据屏幕刷新率自动调整频率,性能更优。
  • 适用于动画、游戏、实时渲染等场景。

2. 数据驱动 vs 响应式更新

  • Vue 的响应式系统会自动追踪依赖,在数据变化时更新视图。
  • 我们使用 displayText 来驱动 DOM 更新,Vue 自动处理虚拟 DOM diff。

3. 清理副作用

  • 在 Vue 中,我们通过 beforeDestroy 钩子取消未完成的动画任务,避免内存泄漏。
  • 这是一个良好的编程习惯。

4. 与父级通信

  • 使用 props 接收配置项(如 fullText, speed)。
  • 通过 setGlobal 回调传递状态给父组件,可用于后续操作(如跳转页面、切换状态)。

🌟 第六步:扩展功能建议

✅ 添加播放/暂停按钮

你可以添加一个按钮来控制播放状态:

<button @click="isPlaying ? pauseStream() : startStream()">
  {{ isPlaying ? '⏸️ 暂停' : '▶️ 开始' }}
</button>

✅ 自动滚动到底部(适合长文本)

如果文本很长,可以监听 renderNextChunk 并自动滚动:

this.$el.scrollIntoView({ behavior: 'smooth', block: 'end' });

✅ 支持富文本格式(HTML)

如果你希望支持 HTML 格式的文本输出,可以使用 v-html 指令:

<div class="stream-content" v-html="displayText"></div>

但要注意 XSS 安全问题。


📌 小结

通过本文你已经掌握了以下技能:

技术点内容
动画实现使用 requestAnimationFrame 实现流畅打字动画
Vue 组件化封装可复用的 Vue2 组件
状态管理控制播放状态、进度、完成回调
性能优化合理使用帧率计算和资源清理
扩展能力支持控制台、滚动、富文本等

📚 参考资料


✅ 结语

文字打印机效果虽然简单,但它融合了动画、响应式、组件设计等多个前端核心概念。掌握它不仅有助于理解 Vue 的响应式机制,也能帮助你在实际项目中提升用户体验。

如果你喜欢这篇文章,欢迎点赞、收藏、分享!也欢迎继续关注我,我会带来更多实用的前端开发教程与实战案例。