首先,什么是打字机效果呢?打字机效果即为文字逐个输出,实际上就是一种Web动画。一图胜千言,诸君请看:
在Web应用中,实现动画效果的方法比较多,JavaScript 中可以通过定时器 setTimeout 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的 API,即 requestAnimationFrame(rAF),顾名思义就是 “请求动画帧”。接下来,我们一起来看看 打字机效果 的几种实现。为了便于理解,我会尽量使用简洁的方式进行实现,有兴趣的话,你也可以把这些实现改造的更有逼格、更具艺术气息一点,因为编程,本来就是一门艺术。
setTimeout版本的实现很简单,只需把要展示的文本进行切割,使用定时器不断向DOM元素里追加文字即可,同时,使用::after伪元素
在DOM元素后面产生光标闪烁的效果。代码和效果图如下:
<!-- 样式 --> <style type="text/css"> /* 设置容器样式 */ #content { height: 400px; padding: 10px; font-size: 28px; border-radius: 20px; background-color: antiquewhite; } /* 产生光标闪烁的效果 */ #content::after{ content: '|'; color:darkgray; animation: blink 1s infinite; } @keyframes blink{ from{ opacity: 0; } to{ opacity: 1; } } </style> <body> <div id='content'></div> <script> (function () { // 获取容器 const container = document.getElementById('content') // 把需要展示的全部文字进行切割 const data = '最简单的打字机效果实现'.split('') // 需要追加到容器中的文字下标 let index = 0 function writing() { if (index < data.length) { // 追加文字 container.innerHTML += data[index ++] let timer = setTimeout(writing, 200) console.log(timer) // 这里会依次打印 1 2 3 4 5 6 7 8 9 10 } } writing() })(); </script> </body>
setTimeout()方法的返回值是一个唯一的数值(ID),上面的代码中,我们也做了setTimeout()返回值的打印,那么,这个数值有什么用呢?
如果你想要终止setTimeout()方法的执行,那就必须使用 clearTimeout()方法来终止,而使用这个方法的时候,系统必须知道你到底要终止的是哪一个setTimeout()方法(因为你可能同时调用了好几个 setTimeout()方法),这样clearTimeout()方法就需要一个参数,这个参数就是setTimeout()方法的返回值(数值),用这个数值来唯一确定结束哪一个setTimeout()方法。
setInterval实现的打字机效果,其实在MDN window.setInterval 案例三中已经有一个了,而且还实现了播放、暂停以及终止的控制,效果可点击这里查看,在此只进行setInterval打字机效果的一个最简单实现,其实代码和前文setTimeout的实现类似,效果也一致。
(function () { // 获取容器 const container = document.getElementById('content') // 把需要展示的全部文字进行切割 const data = '最简单的打字机效果实现'.split('') // 需要追加到容器中的文字下标 let index = 0 let timer = null function writing() { if (index < data.length) { // 追加文字 container.innerHTML += data[index ++] // 没错,也可以通过,clearTimeout取消setInterval的执行 // index === 4 && clearTimeout(timer) } else { clearInterval(timer) } console.log(timer) // 这里会打印出 1 1 1 1 1 ... } // 使用 setInterval 时,结束后不要忘记进行 clearInterval timer = setInterval(writing, 200) })();
和setTimeout一样,setInterval也会返回一个 ID(数字),可以将这个ID传递给clearInterval()或者clearTimeout() 以取消定时器的执行。
在此有必要强调一点:定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。
在动画的实现上,requestAnimationFrame 比起 setTimeout 和 setInterval来无疑更具优势。我们先看看打字机效果的requestAnimationFrame实现:
(function () { const container = document.getElementById('content') const data = '与 setTimeout 相比,requestAnimationFrame 最大的优势是 由系统来决定回调函数的执行时机。具体一点讲就是,系统每次绘制之前会主动调用 requestAnimationFrame 中的回调函数,如果系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是,requestAnimationFrame 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。'.split('') let index = 0 function writing() { if (index < data.length) { container.innerHTML += data[index ++] requestAnimationFrame(writing) } } writing() })();
与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
除了以上三种JS方法之外,其实只用CSS我们也可以实现打字机效果。大概思路是借助CSS3的@keyframes
来不断改变包含文字的容器的宽度,超出容器部分的文字隐藏不展示。
<style> div { font-size: 20px; /* 初始宽度为0 */ width: 0; height: 30px; border-right: 1px solid darkgray; /* Steps(<number_of_steps>,<direction>) steps接收两个参数:第一个参数指定动画分割的段数;第二个参数可选,接受 start和 end两个值,指定在每个间隔的起点或是终点发生阶跃变化,默认为 end。 */ animation: write 4s steps(14) forwards, blink 0.5s steps(1) infinite; overflow: hidden; } @keyframes write { 0% { width: 0; } 100% { width: 280px; } } @keyframes blink { 50% { /* transparent是全透明黑色(black)的速记法,即一个类似rgba(0,0,0,0)这样的值。 */ border-color: transparent; /* #00000000 */ } } </style> <body> <div> 大江东去浪淘尽,千古风流人物 </div> </body>
以上CSS打字机效果的原理一目了然:
border-right
,并在关键帧上改变 border-color
为transparent
,右边框就像闪烁的光标了。Typed.js is a library that types. Enter in any string, and watch it type at the speed you've set, backspace what it's typed, and begin a new sentence for however many strings you've set.
Typed.js是一个轻量级的打字动画库, 只需要几行代码,就可以在项目中实现炫酷的打字机效果(本文第一张动图即为Typed.js实现)。源码也相对比较简单,有兴趣的话,可以到GitHub进行研读。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/typed.js@2.0.11"></script> </head> <body> <div id="typed-strings"> <p>Typed.js is a <strong>JavaScript</strong> library.</p> <p>It <em>types</em> out sentences.</p> </div> <span id="typed"></span> </body> <script> var typed = new Typed('#typed', { stringsElement: '#typed-strings', typeSpeed: 60 }); </script> </html>
使用Typed.js,我们也可以很容易的实现对动画开始、暂停等的控制:
<body> <input type="text" class="content" name="" style="width: 80%;"> <br> <br> <button class="start">开始</button> <button class="stop">暂停</button> <button class="toggle">切换</button> <button class="reset">重置</button> </body> <script> const startBtn = document.querySelector('.start'); const stopBtn = document.querySelector('.stop'); const toggleBtn = document.querySelector('.toggle'); const resetBtn = document.querySelector('.reset'); const typed = new Typed('.content',{ strings: ['雨过白鹭州,留恋铜雀楼,斜阳染幽草,几度飞红,摇曳了江上远帆,回望灯如花,未语人先羞。'], typeSpeed: 200, startDelay: 100, loop: true, loopCount: Infinity, bindInputFocusEvents:true }); startBtn.onclick = function () { typed.start(); } stopBtn.onclick = function () { typed.stop(); } toggleBtn.onclick = function () { typed.toggle(); } resetBtn.onclick = function () { typed.reset(); } </script>
参考资料:Typed.js官网 | Typed.js GitHub地址
当然,打字机效果的实现方式,也不仅仅局限于上面所说的几种方法,本文的目的,也不在于搜罗所有打字机效果的实现,如果那样将毫无意义,接下来,我们将会对CSS3动画和JS动画进行一些比较,并对setTimeout、setInterval 和 requestAnimationFrame的一些细节进行总结。
关于CSS动画和JS动画,有一种说法是CSS动画比JS流畅,其实这种流畅是有前提的。借此机会,我们对CSS3动画和JS动画进行一个简单对比。
优点:
缺点:
优点:
部分情况下浏览器可以对动画进行优化(比如专门新建一个图层用来跑动画),为什么说部分情况下呢,因为是有条件的:
缺点:
CSS动画比较少或者不触发pain和layout,即重绘和重排时。例如通过改变如下属性生成的css动画,这时整个CSS动画得以在compositor thread完成(而JS动画则会在main thread执行,然后触发compositor进行下一步操作):
通过设置 will-change
属性,浏览器就可以提前知道哪些元素的属性将会改变,提前做好准备。待需要改变元素的时机到来时,就可以立刻实现它们,从而避免卡顿等问题。
will-change
应用到太多元素上,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。例如下面的代码就是提前告诉渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。
.box {will-change: transform, opacity;}
setTimeout 的执行只是在内存中对元素属性进行改变,这个变化必须要等到屏幕下次绘制时才会被更新到屏幕上。如果两者的步调不一致,就可能会导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素。假设屏幕每隔16.7ms刷新一次,而setTimeout 每隔10ms设置图像向左移动1px, 就会出现如下绘制过程:
从上面的绘制过程中可以看出,屏幕没有更新 left=2px 的那一帧画面,元素直接从left=1px 的位置跳到了 left=3px 的的位置,这就是丢帧现象,这种现象就会引起动画卡顿。
// repeat with the interval of 2 seconds let timerId = setInterval(() => console.log('tick', timerId), 2000); // after 50 seconds stop setTimeout(() => { clearInterval(timerId); console.log('stop', timerId); }, 50000);
let timerId = setTimeout(function tick() { console.log('tick', timerId); timerId = setTimeout(tick, 2000); // (*) }, 2000);
除了上文提到的requestAnimationFrame的优势外,requestAnimationFrame还有以下两个优势:
2011年的标准中是这么规定的:
往期高分合集:
本文首发于个人博客,欢迎指正和star。