需求分析
- 在 css 中,如果要给一个元素设置动画,就要改变一个css属性,也是一个值到另外一个值的变化,
- 但是放入到我们这里的动画函数里面,我是不知道是具体要用到那个元素上的,所以只能是计算一个数据值到另一个数据值的变化
- 什么变化呢?比如在多少时间内,1-100,变化的频率等等,所以我们只要能实现这些就好了
确定参数
- 一段时间内,一个值到另外一个值,首先就要知道这个一段时间有多长,还需要知道在这个值变化的过程中,例如 1-10,5s的时间,变化的步进值,比如每次前进0.1还是0.5,所以还需要一个间隔时间。比如500ms变化一次,那么每次的步进的就是10/(10000ms/500ms),每次前进 0.5,前进 20 次
- 基于此,我们就确定了四个参数:起始值、结束值、总时间、间隔时间
- 那么这个是不是完成了呢?不然,我每次改变的值,总需要使用吧,这个使用方式,是多种多样的,只能交给使用者来决定,而适合这种灵活的场景,只有是函数了,所以还需要一个参数,回调函数,这个回调函数的执行时机是每次间隔时间后调用一次,调用多少次,取决于传递的参数
确定属性值
-
我们还需要那些属性呢?
-
首先就是计时器的id:timer;然后运动的总次数:count;当前的运动次数:curCount;当前值:curValue
-
所以我们现在可以写出一个大概的函数模型,如下:
function animationFunc({ begin, end, interval = 16, total = 3000, callback }) { let timer = null // 接收定时器ID let count = Math.ceil(total / interval) // 运动次数【向上取整】 let step = Math.abs((end - begin)) / count // 每次运动的步长 let current = begin // 当前的位置 let currentCount = 0 // 当前运动次数 timer = setInterval(() => { // 如果存在回调函数就调用-并传递当前值 callback && callback(current) }, interval) }
具体实现
-
其实到现在,我们的这个插件已经实现的差不多了,只是缺少一些值的计算和停止条件
-
每次执行后次数+1,当前值 = 当前值 + 每次运动的步长,如果当前次数大于或者等于总次数,就停止执行,如下:
function animationFunc({ begin, end, interval = 16, total = 3000, callback }) { let timer = null let count = Math.ceil(total / interval) let step = Math.abs(end - begin) / count let curValue = begin let currentCount = 0 timer = setInterval(() => { // 改变当前值 curValue += step // 次数+1 currentCount++ // 如果当前次数大于或者等于总次数就停止执行 if (currentCount >= count) { // 动画结束 callback && callback(curValue) clearInterval(timer) timer = null return } callback && callback(curValue) }, interval) }
-
那我们具体使用看一下,html 中写了一个使用绝对定位固定的 div,如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .box { position: absolute; left: 0; top: 20px; background-color: skyblue; width: 100px; height: 100px; } </style> </head> <body> <div class="box"></div> <script src="./index.js"></script> </body> </html>
-
看看使用代码,如下:
const box = document.querySelector('.box') // 动画函数 function animationFunc({ begin, end, interval = 16, total = 3000, callback }) { let timer = null let count = Math.ceil(total / interval) let step = Math.abs(end - begin) / count let curValue = begin let currentCount = 0 timer = setInterval(() => { curValue += step currentCount++ if (currentCount >= count) { callback && callback(curValue) clearInterval(timer) timer = null return } callback && callback(curValue) }, interval) } box.onclick = function () { animationFunc({ begin: 0, end: 500, callback: curValue => { box.style.left = curValue + 'px' } }) }
-
执行效果如图:
-
当然经过 gif 的转换和录屏本身的原因,会存在不小的抖动
简单扩展
-
上述我们已经实现了这个动画的函数,而且由于具体实现的业务不为动画函数本身决定,大大增加了灵活程度,但是业务总是千奇百怪的,比如我可能是希望能在动画执行开始和完成的时机调用一个方法,告诉我动画函数执行完成了,此时我们可以多加两个参数 onBegin、onFinish,同时为了具备的更加语义化 callback 改为 onChange,如下:
const box = document.querySelector('.box') // 动画函数 function animationFunc({ begin, end, interval = 16, total = 3000, onChange, onBegin, onFinish }) { // 执行的时候调用一次 onBegin onBegin && onBegin() let timer = null let count = Math.ceil(total / interval) let step = Math.abs(end - begin) / count let curValue = begin let currentCount = 0 timer = setInterval(() => { curValue += step currentCount++ if (currentCount >= count) { // 执行完成后调用 onFinish onFinish && onFinish() onChange && onChange(curValue) clearInterval(timer) timer = null return } onChange && onChange(curValue) }, interval) } box.onclick = function () { animationFunc({ begin: 0, end: 500, onChange: curValue => { box.style.left = curValue + 'px' }, onBegin: () => { box.innerHTML = '开始执行动画' }, onFinish: () => { box.innerHTML = '动画执行完成' } }) }
-
效果如图:
-
至于具体需要利用这两个时机完成什么,或者做点什么,就看各自的业务需求了,还可以根据你自己的需求作出更多扩展,比如传递多个起始值可以执行多组动画
-
当然,考虑严谨性,可以考虑多写一些参数的类型判断或者当前的值如果大于了结束值,就赋值为结束值,如下:
function isFunction(value) { // 为空表示未使用,则默认返回 true if (value === null || value === undefined) return true return typeof value === 'function' } // 动画函数 function animationFunc({ begin, end, interval = 16, total = 3000, onChange, onBegin, onFinish }) { if (!isFunction(onBegin)) { throw new Error('onBegin 必须是函数') } if (!isFunction(onFinish)) { throw new Error('onFinish 必须是函数') } if (!isFunction(onChange)) { throw new Error('onChange 必须是函数') } if (begin === undefined || begin === null) { throw new Error('begin 必须有值') } if (end === undefined || end === null) { throw new Error('end 必须有值') } onBegin && onBegin() let timer = null let count = Math.ceil(total / interval) let step = Math.abs(end - begin) / count let curValue = begin let currentCount = 0 timer = setInterval(() => { curValue += step currentCount++ if (curValue > end) curValue = end if (currentCount >= count) { onFinish && onFinish() onChange && onChange(curValue) clearInterval(timer) timer = null return } onChange && onChange(curValue) }, interval) }