携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情
在上一篇文章中,我们详细描述了如何链式的执行数组中的任务,主要是通过next
函数调用来改变任务链的索引值,然后执行任务。本章主要是解释如何定时的去改变的changePosition
,从而形成动画。
requestAnimationFrame
requestAnimationFrame
字面意思是请求动画帧,也称 帧循环。
requestAnimationFrame 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
我们先初步认识一下它,我们给它传递一个回调函数 test
。
<body>
<h1>requestAnimationFrame API</h1>
<button id='begin' class="begin">开始</button>
<button id='end' class="end">停止</button>
</body>
// js
(() => {
function test() {
console.log('hello ~ requestAnimationFrame');
}
requestAnimationFrame(test)
})()
可以看到,控制台成功的输出了一次 log
。
但是它只执行了一次,怎么做动画呢?别急,再看看 MDN
怎么说。
若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
(() => {
let n = 0
function test() {
n++
console.log(`hello ~ requestAnimationFrame ${n}`)
requestAnimationFrame(test)
}
requestAnimationFrame(test)
})()
可以看到,test
函数会隔一段时间执行一次,它执行次数通常是每秒 60 次,但在大多数遵循 W3C
建议的浏览器中,回调函数执行次数通常与 浏览器屏幕刷新次数 相匹配。
这回就知道了,原来它根本就不用手动设置执行间隔时间,而是根据 浏览器屏幕刷新次数 自动调整了,也就是说浏览器屏幕刷新多少次,它就执行多少次。
终止执行
那如果我想要在特定的条件下终止 requestAnimationFrame
怎么办呢,官方也给出了答案,那就是 cancelAnimationFrame API
。 只需要把 requestAnimationFrame
的返回值作为参数传递给 cancelAnimationFrame
就可以了。
动画的代码实现
- 定义一个timeline的类
var DEFAULT_INTERVAL = 1000 / 60
// 初始化状态
var STATE_INITIAL = 0
// 开始状态
var STATE_START = 1
// 停止状态
var STATE_STOP = 2
function Timeline() {
this.animationHandler = 0
this.state = STATE_INITIAL
}
- 动画开始逻辑
参数interval表示动画执行的事件间隔,如果你想动画快一点可以把interval设置的更小,如果你想让动画慢一点可以设置的更大,如果不设置就默认是浏览器的刷新频率。
/**
* 动画开始
* @param {*} interval 每一次回调的间隔时间,并不一定要求是17ms
*/
Timeline.prototype.start = function (interval) {
if (this.state === STATE_START) {
return
}
this.state = STATE_START
// 默认是1秒中刷新60次 1000/60
this.interval = interval || DEFAULT_INTERVAL
startTimeline(this, +new Date())
}
function startTimeline(timeline, startTime) {
timeline.startTime = startTime
// 记录上一次回调的函数
var lastTick = +new Date()
nextTick.interval = timeline.interval
nextTick()
/**
* 每一帧执行的函数
*/
function nextTick() {
var now = +new Date()
timeline.animationHandler = requestAnimationFrame(nextTick)
// 当前时间和上一次动画回调的时间差大于设置的事件间隔interval,表示这次可以执行回调函数onenterframe
if (now - lastTick >= timeline.interval) {
timeline.onenterframe(now - startTime)
lastTick = now
}
}
}
下面代码保证nextTick
会循环执行:
nextTick()
function nextTick() {
timeline.animationHandler = requestAnimationFrame(nextTick)
}
下面代码保证了,在我们规定的时间间隔内执行一次nextTick
if (now - lastTick >= timeline.interval) {
// 动画的具体逻辑,由外部定义
timeline.onenterframe(now - startTime)
lastTick = now
}
下面的代码保证了外部可以定义onenterframe
函数逻辑:
Timeline.prototype.onenterframe = function (time) {}
onenterframe
具体实现如下
Animation.prototype._asyncTask = function (task) {
var me = this
// 定义每一帧执行的回调函数
var enterFrame = function (time) {
var taskFn = task.taskFn
var next = function () {
// 停止当前任务
me.timeline.stop()
// 执行下一个任务
me._next(task)
}
taskFn(next, time)
}
this.timeline.onenterframe = enterFrame
this.timeline.start(this.interval)
}
也就是如果满足interval的时间间隔,那么就不断的执行的enterFrame
函数,在这个动画中enterFrame
函数的具体逻辑就是不断改变background-position:
// time是动画从开始到目前的时间,通过这个时间和interval来计算index
taskFn = function (next, time) {
if (imgUrl) {
ele.style.backgroundImage = 'url(' + imgUrl + ')'
}
// 获得当前背景图片的位置索引
// | 0 等于Math.floor
var index = Math.min((time / me.interval) | 0, len)
var position = positions[index - 1].split(' ')
// 改变dom对象的背景图片位置
ele.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px'
if (index === len) {
next()
}
}
总结
timeline类的主要功能是利用requestAnimationFrame
来实现一个不断循环执行一段逻辑的功能,执行的时间间隔是外界传入的,只要达到now - lastTick >= timeline.interval
的条件就去执行动画的逻辑。
然后在animation
类中定义动画执行的逻辑,比如changePosition
。