总结
只打印单纯的文字
只打印文字时,就是循环打印字符串,字符串长度依次递增
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="text">
</div>
</body>
<script>
let divTyping = document.getElementById('text')
let i = 0,
timer = 0,
str = '用JS实现动态打字效果'
function typing () {
if (i <= str.length) {
divTyping.innerHTML = str.slice(0, i++) + '_'
timer = setTimeout(typing, 200)
}
else {
divTyping.innerHTML = str//结束打字,移除 _ 光标
clearTimeout(timer)
}
}
typing()
</script>
</html>
如果只需要打印文字,上面的函数就足够用了。缺陷就是只能打印文字。
### 打印带标签的段落
<div id="source">
打印带有标签的段落
<p>我是段落</p>
<ul>
<li>列表1</li>
<li>列表2</li>
<li>列表3</li>
</ul>
</div>
上面的方法是我参考的最牛的打字效果JS插件 typing.js,我自己理解后用ES6语法重新写了一遍,毕竟ES6是主流,并且有相当好工具去编译ES6。
原理
获取要打印的内容
将内容转化为字符和对象组成的数组中(对象是用来保存dom节点的)
获取数组的第一个元素并在原数组中删除,然后判断:是字符则打印;是对象,则创建dom节点,并重复第一步
数组长度为空时,判断对象是否有parent属性,有则回到第一步,没有则打印完毕
结合上面的例子我再重复一遍流程:
1.获取要打印的内容(id为source的div中的内容)
这里既有文本节点有有dom节点,将文本节点转化为字符串数组,将dom节点保存为对象,最后合并为一个数组
然后依次打印数组中的字符,若遇见保存dom的对象,在先创建节点再打印节点内容(节点内容中可能还会有dom节点),就这样依次打印完毕了
因为是克隆的节点,所以节点的样式都会存在
源码
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<div id="source">
打印带有标签的段落
<p>我是段落</p>
<ul>
<li>列表1</li>
<li>列表2</li>
<li>列表3</li>
</ul>
</div>
<div id="output">
</div>
<script type="text/javascript">
class Typing {
constructor(opts) {
this.opts = opts || {};
this.source = opts.source;
this.output = opts.output;
this.delay = opts.delay || 120;
this.chain = {
parent: null,
dom: this.output,
val: []
};
if (!(typeof this.opts.done === 'function')) this.opts.done = function () {
};
}
init() {
//初始化函数
this.chain.val = this.convert(this.source, this.chain.val);
}
convert(dom, arr) {
//将dom节点的子节点转换成数组,
let children = Array.from(dom.childNodes)
for (let i = 0; i < children.length; i++) {
let node = children[i]
if (node.nodeType === 3) {
arr = arr.concat(node.nodeValue.split('')) //将字符串转换成字符串数组,后面打印时才会一个一个的打印
} else if (node.nodeType === 1) {
let val = []
val = this.convert(node, val)
arr.push({
'dom': node,
'val': val
})
}
}
return arr
}
print(dom, val, callback) {
setTimeout(function () {
dom.appendChild(document.createTextNode(val));
callback();
}, this.delay);
}
play(ele) {
//当打印最后一个字符时,动画完毕,执行done
if (!ele.val.length) {
if (ele.parent) this.play(ele.parent);
else this.opts.done();
return;
}
let current = ele.val.shift() //获取第一个元素,同时删除数组中的第一个元素
if (typeof current === 'string') {
this.print(ele.dom, current, () => {
this.play(ele); //继续打印下一个字符
})
} else {
let dom = current.dom.cloneNode() //克隆节点,不克隆节点的子节点,所以不用加参数true
ele.dom.appendChild(dom)
this.play({
parent: ele,
dom,
val: current.val
})
}
}
start() {
this.init();
this.play(this.chain);
}
}
</script>
<script type="text/javascript">
let source = document.getElementById('source')
let output = document.getElementById('output')
let typing = new Typing({
source,
output
})
typing.start()
</script>
</body>
</html>