[Toc]
显示屏的刷新频率与动画原理简介
- 过去没有显示屏的年代里,存在一种叫作连环画的东西,它的每一页都将人物在某一指定时间的动作详细地画出来,读者可以通过快速翻看连环画来达到一种画面中的人物在连贯运动的效果。
- 这种现象后来被总结为"视觉停留效应",即人眼所看到的影像消失后,大脑仍然会保存这一图像一段时间(可以简单理解为作为接收光信号的眼球,与处理信号的大脑,之间的信号传递存在一定时间的延迟),这一时间大概是1/24秒。也就是说,如果连环画要保持连贯没有卡顿的效果,那么最少需要每秒翻过24页,才能让连环画上的静态人物在视觉上“动”起来。
- 后来这一效果运用到了显示屏上。在如今的的LCD显示屏上,通过改变显示屏上的像素的电压来达到更改像素显示颜色的目的,而这一频率大多维持在60hz左右。也就是说就算什么都不做,显示屏也会以每秒60次的频率刷新屏幕。而因为60hz的更新频率远远大于动画最低的24hz,因此这种切换在人眼看来是连贯的。
setTimeout与setInterval在动画表现上的问题
在过去的前端开发中经常使用setInterval制作动画。但
setInterval(callback,delay)
的执行方式是在delay毫秒后,将回调函数扔进事件循环队列中,等待同步代码执行结束后才会去事件循环队列中以“先进先出”的方式执行回调函数,而扔进事件循环队列中回调函数并不一定排在首位,因此回调函数的执行时间一般都比delay设定的时间晚,且无法确定。-
setTimeout与setInterval在动画表现上的作用仅在于修改具体DOM的属性,这个变化必须等到下次屏幕重绘才能实现。如果在两次屏幕刷新的间隙中,存在多次修改DOM属性,但是刷新屏幕仅表现最近一次的所修改的属性,这样就造成了中间某些步骤会被省略掉,这就是丢帧现象。
let left=0 setIntervacl(function(){ left++ oDiv.style.left=left+px },,10)
-
上面的代码以每秒100次的频率修改DOM属性,而屏幕以每秒60次(16.7ms一次)的频率刷新。那么存在:
- 第0ms:left值为0,屏幕未刷新
- 第10ms:left值为1,屏幕未刷新
- 第16.7ms:left值为1,屏幕刷新,表现为{left:1px}
- 第20ms:left值为2,屏幕未刷新。
- 第30ms:left值为3,屏幕未刷新。
- 第33.4ms:left值为3,屏幕刷新,表现为{left:3px}
通过上面的列举可以看到,{left:2px}的渲染被跳过了,div的属性表现直接从1px跳至3px,失去了{left:2px}这一帧。
requestAnimationFrame的原理与使用
回调函数的调用时机
let o = 0 let start = Date.now() setTimeout(() => { console.log('下一个tick') }, 0); window.requestAnimationFrame(function () { console.log('间隔:' + (Date.now() - start)) }) for (let i = 0; i < 100000000; i++) { o++ } // 间隔:252 // 下一个tick
- 可以看到,
window.requesetAnimationFrame
的回调函数是在setTimeout
回调函数之前执行的。而setTimeout
的回调函数是扔进了事件循环队列中。因此可以确定requestAnimationFrame
的回调函数是在本次tick的任务队列中执行的,并没有加入到事件循环队列中。
API的使用
基于“在屏幕刷新间隙中多次执行回调函数是无意义的”这一认知。
requestAnimationFrame
API要求由系统来决定回调函数的执行时机。如果屏幕刷新频率是60hz,那么连续两次调用requestAnimationFrame
时,第二次回调函数的调用时机在第一次调用后的下一次屏幕刷新后。确保每一次屏幕刷新只会调用一次回调函数。let oDiv = document.querySelector('#d1') let left = 0 let count = 0 let timeStart = 0 // 第一次调用回调函数的时间 let timeLast = 0 // 上一次调用回调函数的时间 let timeEnd = 0 // 最后调用回调函数的时间 window.requestAnimationFrame(function () { let fn = arguments.callee count++ left += 10 // 每一帧向右偏移10px oDiv.style.transform = 'translateX(' + left + 'px)' let now = Date.now() if (count === 1) { timeStart = now timeLast = timeStart console.log('初始调用时间:' + timeStart) } console.group(`第${count}次调用回调函数`) console.log(`调用时间: ${now}`) console.log(`与上一次调用时间的间隔: ${now-timeLast}`) console.log(`下一次刷新偏的位置: ${left}`) console.groupEnd() if (left < 500) { timeLast = now window.requestAnimationFrame(fn) } else if (left === 500) { timeEnd = now console.log('平均调用时间:' + (timeEnd - timeStart) / (count - 1)) // 屏幕只刷新了49次 } })
- 通过上面的代码的打印可以看到,刷新频率接近每秒60帧(16.67ms刷新一次)