这篇文章来源于之前的一个活动需求,有一个动画效果是数字跳动递增,但是这个数字不能是文本,而是0-9的10张图片,所以组件库中的文本数字跳动是不能直接用了,就不得不重新写一个,在递增的基础上又增加了递减的动画,实际效果是这样的:
不好截图想象一下吧。:)
最开始我是用 setInterval 写的,只有递增动画,没有递减动画,而且动画很卡顿,需要在数字跳动到某一个数字的时候更换成图片,而且这些图片会在数字频繁的变化后去请求图片,然后再取消请求,性能很差,至于原因可以看 mdn 上 setInterval 的介绍:
setInterval() 方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟。这个延迟的时间并不一定是设置的时间,实际的延迟时间会稍微长一点。
而且图片的请求和取消请求对性能影响也很差所有就考虑将图片转化成 base64 来做,性能有很大的提升,接下来一步步实现优化吧。
动画优化
requestAnimationFrame()
是告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
。
回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame()
运行在后台标签页或者隐藏的<iframe>
里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命
,可以看到 requestAnimationFrame 的优势是在后台运行或者元素不可见的时候动画会被暂停,会提高渲染性能和电池寿命。
使用 setInterval 实现
import React, { Component } from 'react'
import './index.css';
export default class ByteDance extends Component {
constructor(props){
super(props);
this.state = {
innerNode: null,
number: 0,
}
this.timer = null;
this.step = 0;
}
componentDidMount() {
const { overNumber, duration } = this.props;
this.step = (overNumber - this.state.number) / (duration / 16.6);
console.log('componentDidMount step', this.step)
this.timer = setInterval(() => {
this.tick()
}, 16.6)
}
componentDidUpdate(prevProps) {
if (prevProps.overNumber !== this.props.overNumber) {
this.step = Math.abs(this.props.overNumber - this.state.number) / (this.props.duration / 16.6);
console.log('componentDidUpdate step', this.step)
this.timer = setInterval(() => {
this.tick()
}, 16.6)
}
}
tick = () => {
if (this.state.number < this.props.overNumber) {
// 递增
this.setState({
number: this.state.number + this.step
},()=>{
if (this.state.number > this.props.overNumber) {
clearInterval(this.timer)
this.setState({
number: this.props.overNumber
})
}
});
}
else if (this.state.number > this.props.overNumber) {
// 递减
this.setState({
number: this.state.number - this.step
});
}
else {
this.setState({
number: this.props.overNumber
});
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
return (
<div>
{
(parseInt(this.state.number)+'').split('').map((item,index)=><img className="imageNumber" key={index} src={this.props.images[parseInt(item)]} alt="" />)
}
</div>
)
}
}
可以将两种组件分别接入业务中,可以看到是有差别的,setInterval并不是准时的渲染,目前这种单个测试,可能看不出来特别的变化。
使用 requestAnimationFrame 实现
import React, { Component } from 'react'
import { getBase64Image } from './util'
import './index.css';
class ByteDance extends Component {
constructor(props) {
super()
this.state = {
number: 0
}
componentDidMount() {
//步长=总数/(持续时间/一帧的时间)
this.step = (this.props.overNumber - this.state.number) / (this.props.duration / 16.6);
console.log('componentDidMount step', this.step)
requestAnimationFrame(this.tick);
}
componentDidUpdate(prevProps) {
if (prevProps.overNumber !== this.props.overNumber) {
this.step = Math.abs(this.props.overNumber - this.state.number) / (this.props.duration / 16.6);
console.log('componentDidUpdate step', this.step)
requestAnimationFrame(this.tick);
}
}
tick = ()=>{
if (this.state.number < this.props.overNumber) {
// 递增
this.setState({
number: this.state.number + this.step
},()=>{
if (this.state.number > this.props.overNumber) {
this.setState({
number: this.props.overNumber
})
return;
}
requestAnimationFrame(this.tick);
});
}
else if (this.state.number > this.props.overNumber) {
// 递减
this.setState({
number: this.state.number - this.step
},()=>{
requestAnimationFrame(this.tick);
});
}
else {
this.setState({
number: this.props.overNumber
});
}
}
render() {
return (
<div>
{ (parseInt(this.state.number)+'').split('').map((item,index)=><img className="imageNumber" key={index} src={this.props.images[parseInt(item)]} alt="" />)
}
</div>
)
}
}
export default ByteDance;
从网络请求来看,还是有很大的优化空间,图片在数字变动的时候频繁发起和取消请求:
优化办法是:将图片转为base64格式
图片转为 base64 格式
这一块可以单独提炼出来,可以作为一个工具函数用:
const getBase64Image = img => {
var canvas = document.createElement("canvas")
canvas.width = img.width
canvas.height = img.height
var ctx = canvas.getContext("2d")
ctx.drawImage(img, 0, 0, img.width, img.height)
var ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase()
var dataURL = canvas.toDataURL("image/" + ext)
return dataURL
}
const loadImages = (images, resolve) => {
const base64ImagesPromises = []
for(let i = 0; i < images.length; i++) {
const Img = new Image()
Img.setAttribute("crossOrigin",'Anonymous')
Img.src = images[i]
Img.onload = () => {
let dataUrl = getBase64Image(Img)
base64ImagesPromises.push(resolve(dataUrl))
}
}
return base64ImagesPromises
}
// base64 图片按顺序转化
const transformImgToBase64 = (images) => {
const promises = images.map(image => new Promise((resolve,reject) => {
return loadImages([image],resolve)
}))
return Promise.all(promises)
}
export {
transformImgToBase64
}
然后改造这种实现方法,引入 转化好的 base64 格式的数组
import { transformImgToBase64 } from './util'
...
constructor(props) {
super()
this.state = {
number: 0
}
this.base64Images = []
}
async componentDidMount() {
// base64格式
this.base64Images = await transformImgToBase64(this.props.images)
//步长=总数/(持续时间/一帧的时间)
this.step = (this.props.overNumber - this.state.number) / (this.props.duration / 16.6)
console.log('componentDidMount step', this.step)
requestAnimationFrame(this.tick)
}
...
在看下效果,动画便得更流畅了,图片加载一次,然后跳动的时候从缓存中读取:
结束。
网友评论