前言
在网上经常看到很酷炫的打字机动画,很好奇怎么实现的,今天就来尝试实现一下。
最终效果是这样的:
html代码
部分代码
<template>
<div class="page-list">
<div class="answer-content" v-html="content"></div>
<div class="search-block" ref="searchBlock">
<div class="search-content">
<input
type="text"
v-ios-focus
placeholder="描述您想问的问题"
class="search-keyword"
@keyup.enter="searchInfo"
v-model="question"
/>
<img @click="searchInfo" src="@/assets/img/icon/input-send.png" alt="" class="icon-search" />
</div>
</div>
</div>
</template>
data函数
data() {
return {
// 测试数据
testHtml: `<h1>Styled Table Example</h1>
<table style="width: 80%; margin: 20px auto; border-collapse: collapse; font-family: Arial, sans-serif;">
<thead>
<tr>
<th style="border: 2px solid #4CAF50; padding: 12px; text-align: center; background-color: #4CAF50; color: white;">Header 1</th>
<th style="border: 2px solid #4CAF50; padding: 12px; text-align: center; background-color: #4CAF50; color: white;">Header 2</th>
<th style="border: 2px solid #4CAF50; padding: 12px; text-align: center; background-color: #4CAF50; color: white;">Header 3</th>
</tr>
</thead>
<tbody>
<tr style="background-color: #f2f2f2;">
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 1</td>
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 2</td>
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 3</td>
</tr>
<tr>
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 4</td>
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 5</td>
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 6</td>
</tr>
<tr style="background-color: #f2f2f2;">
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 7</td>
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 8</td>
<td style="border: 2px solid #4CAF50; padding: 12px; text-align: center;">Data 9</td>
</tr>
</tbody>
</table>`,
content: '', // 展示的内容
getContentFn: null,
printInterval: null
}
}
思路分析
我们想要的效果是内容部分逐个展示出来,我们会想到通过获取字符串长度,设置定时器,字符累加显示到页面,这样有个问题,我们要展示的内容包含了html标签,字符累加标签会在页面闪现,这不是我们想要的效果,标签字符(例如<div style=“height: 20px;”)不需要逐个展示,而是全部累加,累加过程请看下图:
引入的方法
/**
* @description: 判断是否匹配到
* @regArr : 匹配数组
* @queryMsg : 匹配的字符串
*/
function hasMatch(regArr, queryMsg) {
let index = 0
const isMatch = regArr.some((reg, i) => {
if (reg.test(queryMsg)) {
index = i
return true
}
return false
})
return {
index,
isMatch
}
}
/**
* @description: 替换对应字符
* @queryMsg : 要解析的字符串
* @content : 展示的字符串
*/
export function getDomText() {
// 这里是定义的一些正则,用来匹配标签
const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 标签开始的前半部分('</div')
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 标签开始的后半部分('>'或者'/>')
const startTagClose = /^\s*(\/?)>/
// 闭合标签('</div>')
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const arr = [startTagOpen, startTagClose, endTag]
// 这里定义一个标识,代表标签开始的后半部分已经添加('>'或者'/>')
let isClosed = true
return function getContent(queryMsg, content, isReset) {
if(isReset) {
// 重新加载内容时重置标识
isClosed = true
}
if (!queryMsg) {
// 内容为空时返回
return {
queryMsg,
content
}
}
// 获取匹配到标签的索引和是否匹配到标识
let { index = 0, isMatch } = hasMatch(arr, queryMsg)
// 匹配到标签
if (isMatch) {
const startArr = queryMsg.match(arr[index])
let leng = startArr[0].length
content += startArr[0]
queryMsg = queryMsg.slice(leng)
if (index === 0) {
// 匹配到标签开始的前半部分(比如'</span'),说明需要继续累加下一个字符
isClosed = false
}
if (index === 1) {
isClosed = true
}
} else {
const str = queryMsg.slice(0, 1)
content += str
queryMsg = queryMsg.slice(1)
// 如果已经存在关闭标签'>',则说明截取的是文本内容,直接返回
if (isClosed && str !== ' ') {
return {
queryMsg,
content
}
}
}
// 递归累加字符,如果当前添加的是标签字符,需要继续添加下一个字符,一直到添加的是文本字符则跳出函数
// 说明: '<span style="width: 30px;">文本字符内容</span>'
// 其中 '<span style="width: 30px;">'是标签字符, '文本字符内容'是文本字符
const obj = getContent(queryMsg, content)
return {
queryMsg: obj.queryMsg,
content: obj.content
}
}
}
/**
* @description: 定时器
* @delay : 延时时间
* @fn 执行的回调函数
*/
export function printInterval(delay = 40) {
let timer = null
return function(fn) {
let context = this
clearInterval(timer)
timer = setInterval(() => {
// 注意这里fn函数需要指定this指向,并且需要有返回值,返回值为ture说明字符已经加载完成,然后清除定时器
const success = fn.call(context)
if (success) {
// 内容加载完成清除定时器
clearInterval(timer)
}
console.log('success===>', success)
}, delay)
}
}
vue中方法
mounted() {
this.getContentFn = getDomText()
this.printIntervalFn = printInterval()
},
beforeDestroy () {
// 方法使用了闭包,这里释放一下内存
this.getContentFn = null
this.printIntervalFn = null
},
methods: {
searchInfo() {
this.myPrint(this.testHtml)
},
// 滚动到聊天内容底部
scrollToBottom() {
this.$nextTick(() => {
// 滚动页面到底部
// 由于页面过长,questionContent 对应的标签没有贴出来
const scrollDom = this.$refs.questionContent
scrollDom.scrollIntoView({
behavior: 'smooth',
block: 'end'
})
})
},
// 实现打字机效果
myPrint(queryMsg) {
let currentLen = 0
const rowNum = 15 // 每15个字符调用一下滚动方法
this.printIntervalFn.call(this, function() {
if (!this.getContentFn) {
// 离开当前页面,方法被注销,需要清除定时器
return true
}
const contentObj = this.getContentFn(queryMsg, this.content, true)
// console.log('contentObj=>', contentObj.content)
this.content = contentObj.content
queryMsg = contentObj.queryMsg || ''
// 换行
currentLen++
if (currentLen >= rowNum) {
this.scrollToBottom()
currentLen = 0
}
// 加载完成,清除定时器
return queryMsg === ''
})
},
}
总结
本文叙述了使用js实现打字机效果的一种思路,通过实现打字机效果加深了对正则、递归、闭包、call等知识点的理解,网上还有很多种实现方式,例如使用css,但是这种实现方法只限使用文本字符串,除此之外还有typed.js,typed.js库实现原理跟本文叙述类似。如果小伙伴们有更好的实现方式,欢迎在评论区告知,我也去学习一下。