乐趣区

关于chrome:从打字机效果的-N-种实现看JS定时器机制和前端动画

  首先,什么是打字机成果呢?一图胜千言,诸君请看:

  打字机成果即为文字一一输入,实际上就是 Web 动画。在 Web 利用中,实现动画成果的办法比拟多,JavaScript 中能够通过定时器 setTimeout 来实现,css3 能够应用 transition 和 animation 来实现,html5 中的 canvas 也能够实现。除此之外,html5 还提供一个专门用于申请动画的 API,即 requestAnimationFrame(rAF),顾名思义就是“申请动画帧”。接下来,咱们一起来看看 打字机成果 的几种实现。为了便于了解,我会尽量应用简洁的形式进行实现,有趣味的话,你也能够把这些实现革新的更有逼格、更具艺术气味一点,因为编程,原本就是一门艺术。

打字机成果的 N 种实现

实现一:setTimeout()

  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()

  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()

  在动画的实现上,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 的步调跟着零碎的刷新步调走。 它能保障回调函数在屏幕每一次的刷新距离中只被执行一次,这样就不会引起丢帧景象,也不会导致动画呈现卡顿的问题

实现四:CSS3

  除了以上三种 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 打字机成果的原理高深莫测:

  • 初始文字是全副在页面上的,只是容器的宽度为 0,设置文字超出局部暗藏,而后一直扭转容器的宽度;
  • 设置border-right,并在关键帧上扭转 border-colortransparent,左边框就像闪动的光标了。

实现五:Typed.js

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 的一些细节进行总结。

CSS3 动画和 JS 动画的比拟

  对于 CSS 动画和 JS 动画,有一种说法是 CSS 动画比 JS 晦涩,其实这种晦涩是有前提的。借此机会,咱们对 CSS3 动画和 JS 动画进行一个简略比照。

JS 动画

  • 长处:

    • JS 动画控制能力强,能够在动画播放过程中对动画进行精密管制,如开始、暂停、终止、勾销等;
    • JS 动画成果比 CSS3 动画丰盛,性能涵盖面广,比方能够实现曲线运动、冲击闪动、视差滚动等 CSS 难以实现的成果;
    • JS 动画大多数状况下没有兼容性问题,而 CSS3 动画有兼容性问题;
  • 毛病:

    • JS 在浏览器的主线程中运行,而主线程中还有其它须要运行的 JS 脚本、款式计算、布局、绘制工作等,对其烦扰可能导致线程呈现阻塞,从而造成丢帧的状况;
    • 对于帧速体现不好的低版本浏览器,CSS3 能够做到天然降级,而 JS 则须要撰写额定代码;
    • JS 动画往往须要频繁操作 DOM 的 css 属性来实现视觉上的动画成果,这个时候浏览器要不停地执行重绘和重排,这对于性能的耗费是很大的,尤其是在调配给浏览器的内存没那么拮据的挪动端。

CSS3 动画

  • 长处:

    • 局部状况下浏览器能够对动画进行优化(比方专门新建一个图层用来跑动画),为什么说局部状况下呢,因为是有条件的:

      • 在 Chromium 根底上的浏览器中
      • 同时 CSS 动画不触发 layout 或 paint,在 CSS 动画或 JS 动画触发了 paint 或 layout 时,须要 main thread 进行 Layer 树的重计算,这时 CSS 动画或 JS 动画都会阻塞后续操作。
    • 局部成果能够强制应用硬件加速(通过 GPU 来进步动画性能)
  • 毛病:

    • 代码简短。CSS 实现略微简单一点动画,CSS 代码可能都会变得十分轻便;
    • 运行过程管制较弱。css3 动画只能在某些场景下管制动画的暂停与持续,不能在特定的地位增加回调函数。

main thread(主线程)和 compositor thread(合成器线程)

  • 渲染线程分为 main thread(主线程)和 compositor thread(合成器线程)。主线程中保护了一棵 Layer 树(LayerTreeHost),治理了 TiledLayer,在 compositor thread,保护了同样一颗 LayerTreeHostImpl,治理了 LayerImpl,这两棵树的内容是拷贝关系。因而能够彼此不烦扰,当 Javascript 在 main thread 操作 LayerTreeHost 的同时,compositor thread 能够用 LayerTreeHostImpl 做渲染。当 Javascript 忙碌导致主线程卡住时,合成到屏幕的过程也是晦涩的。
  • 为了实现防假死,鼠标键盘音讯会被首先散发到 compositor thread,而后再到 main thread。这样,当 main thread 忙碌时,compositor thread 还是可能响应一部分音讯,例如,鼠标滚动时,如果 main thread 忙碌,compositor thread 也会解决滚动音讯,滚动曾经被提交的页面局部(未被提交的局部将被刷白)。

CSS 动画比 JS 动画晦涩的前提

  • CSS 动画比拟少或者不触发 pain 和 layout,即重绘和重排时。例如通过扭转如下属性生成的 css 动画,这时整个 CSS 动画得以在 compositor thread 实现(而 JS 动画则会在 main thread 执行,而后触发 compositor 进行下一步操作):

    • backface-visibility:该属性指定当元素反面朝向观察者时是否可见(3D,试验中的性能);
    • opacity:设置 div 元素的不通明级别;
    • perspective 设置元素视图,该属性只影响 3D 转换元素;
    • perspective-origin:该属性容许您扭转 3D 元素的底部地位;
    • transform:该属性利用于元素的 2D 或 3D 转换。这个属性容许你将元素旋转,缩放,挪动,歪斜等。
  • JS 在执行一些低廉的工作时,main thread 忙碌,CSS 动画因为应用了 compositor thread 能够放弃晦涩;
  • 局部属性可能启动 3D 减速和 GPU 硬件加速,例如应用 transform 的 translateZ 进行 3D 变换时;
  • 通过设置 will-change 属性,浏览器就能够提前晓得哪些元素的属性将会扭转,提前做好筹备。待须要扭转元素的机会到来时,就能够立即实现它们,从而防止卡顿等问题。

    • 不要将 will-change 利用到太多元素上,如果适度应用的话,可能导致页面响应迟缓或者耗费十分多的资源。
    • 例如上面的代码就是提前通知渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候 渲染引擎会将该元素独自实现一帧,等这些变换产生时,渲染引擎会通过合成线程间接去解决变换,这些变换并没有波及到主线程,这样就大大晋升了渲染的效率。

      .box {will-change: transform, opacity;}

setTimeout、setInterval 和 requestAnimationFrame 的一些细节

setTimeout 和 setInterval

  • setTimeout 的执行工夫并不是确定的。在 JavaScript 中,setTimeout 工作被放进了异步队列中,只有当主线程上的工作执行完当前,才会去查看该队列里的工作是否须要开始执行,所以 setTimeout 的理论执行机会个别要比其设定的工夫晚一些。
  • 刷新频率受 屏幕分辨率 和 屏幕尺寸 的影响,不同设施的屏幕绘制频率可能会不同,而 setTimeout 只能设置一个固定的工夫距离,这个工夫不肯定和屏幕的刷新工夫雷同。
  • setTimeout 的执行只是在内存中对元素属性进行扭转,这个变动必须要等到屏幕下次绘制时才会被更新到屏幕上。如果两者的步调不统一,就可能会导致两头某一帧的操作被逾越过来,而间接更新下一帧的元素。假如屏幕每隔 16.7ms 刷新一次,而 setTimeout 每隔 10ms 设置图像向左挪动 1px,就会呈现如下绘制过程:

    • 第 0 ms:屏幕未绘制,期待中,setTimeout 也未执行,期待中;
    • 第 10 ms:屏幕未绘制,期待中,setTimeout 开始执行并设置元素属性 left=1px;
    • 第 16.7 ms:屏幕开始绘制,屏幕上的元素向左挪动了 1px,setTimeout 未执行,持续期待中;
    • 第 20 ms:屏幕未绘制,期待中,setTimeout 开始执行并设置 left=2px;
    • 第 30 ms:屏幕未绘制,期待中,setTimeout 开始执行并设置 left=3px;
    • 第 33.4 ms:屏幕开始绘制,屏幕上的元素向左挪动了 3px,setTimeout 未执行,持续期待中;

  从下面的绘制过程中能够看出,屏幕没有更新 left=2px 的那一帧画面,元素间接从 left=1px 的地位跳到了 left=3px 的的地位,这就是 丢帧景象,这种景象就会引起动画卡顿。

  • setInterval 的回调函数调用之间的理论提早小于代码中设置的提早,因为回调函数执行所需的工夫“耗费”了距离的一部分,如果回调函数执行工夫长、执行次数多的话,误差也会越来越大
// 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);

  • 嵌套的 setTimeout 能够保障固定的提早:
let timerId = setTimeout(function tick() {console.log('tick', timerId);
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);

requestAnimationFrame

  除了上文提到的 requestAnimationFrame 的劣势外,requestAnimationFrame 还有以下两个劣势:

  • CPU 节能:应用 setTimeout 实现的动画,当页面被暗藏或最小化时,setTimeout 依然在后盾执行动画工作,因为此时页面处于不可见或不可用状态,刷新动画是没有意义的,齐全是节约 CPU 资源。而 requestAnimationFrame 则齐全不同,当页面处于未激活的状态下,该页面的屏幕刷新工作也会被零碎暂停,因而跟着零碎步调走的 requestAnimationFrame 也会进行渲染,当页面被激活时,动画就从上次停留的中央继续执行,无效节俭了 CPU 开销。
  • 函数节流:在高频率事件 (resize,scroll 等) 中,为了避免在一个刷新距离内产生屡次函数执行,应用 requestAnimationFrame 可 保障每个刷新距离内,函数只被执行一次,这样既能保障流畅性,也能更好的节俭函数执行的开销。一个刷新距离内函数执行屡次是没有意义的,因为显示器每 16.7ms 刷新一次,屡次绘制并不会在屏幕上体现进去。

对于最小工夫距离

  • 2011 年的规范中是这么规定的:

    • setTimeout:如果以后正在运行的工作是由 setTimeout()办法创立的工作,并且工夫距离小于 4ms,则将工夫距离减少到 4ms;
    • setInterval:如果工夫距离小于 10ms,则将工夫距离减少到 10ms。
  • 在最新规范中:如果工夫距离小于 0,则将工夫距离设置为 0。如果 嵌套级别大于 5,并且工夫距离小于 4ms,则将工夫距离设置为 4ms。

定时器的革除

  • 因为 clearTimeout()和 clearInterval()革除的是同一列表(流动计时器列表)中的条目,因而能够应用这两种办法革除 setTimeout()或 setInterval()创立的计时器。

参考资料

  • HTML — 8.6 Timers
  • requestAnimationFrame
  • CSS3 动画和 JS 动画的比拟
  • Scheduling: setTimeout and setInterval

往期高分合集:

  • 前端装逼技巧 108 式(一)—— 打工人
  • 前端装逼技巧 108 式(二)—— 不讲武德
  • 前端装逼技巧 108 式(三)—— 冇得感情的 API 调用工程师
  • 浏览器是如何工作的:Chrome V8 让你更懂 JavaScript
  • 能够迭代大部分数据类型的 for…of 为什么不能遍历一般对象?

本文首发于集体博客,欢送斧正和 star。

退出移动版