動畫
web開發(fā)中實現(xiàn)動畫的方式有多種,CSS3中的transition和animation,js中的setInterval、setTimeout、canvas。H5新API requestAnimationFrame。
屏幕的刷新頻率
屏幕的刷新頻率即圖像在屏幕上每秒更新的次數(shù),單位為HZ。該值受屏幕分辨率、屏幕尺寸和顯卡的影響。每一次稱之為幀,一般的屏幕為60HZ,則一幀的時間為16.7ms左右。
setInterval動畫存在的問題
setInterval 其實就是通過設(shè)置一個間隔時間來不斷的改變圖像的位置,從而達到動畫效果的。但有時候會發(fā)現(xiàn),利用setInterval實現(xiàn)的動畫在某些低端機上會出現(xiàn)卡頓、抖動的現(xiàn)象。 這種現(xiàn)象的產(chǎn)生有兩個原因:
setInterval 的執(zhí)行時間并不是確定的。js執(zhí)行時,setInterval 會被放進了異步隊列中,只有當主線程上的任務(wù)執(zhí)行完以后,才會去檢查該隊列里的任務(wù)是否需要開始執(zhí)行,因此 setInterval 的實際執(zhí)行時間一般要比其設(shè)定的時間晚一些。
屏幕的刷新頻率受分辨率和尺寸的影響,因此不同設(shè)備的屏幕刷新頻率可能會不同,而 setInterval 只能設(shè)置一個固定的時間間隔,這個時間不一定和屏幕的刷新時間相同,所以會引起丟幀從而出現(xiàn)卡頓現(xiàn)象。
requestAnimationFrame
window.requestAnimationFrame() 告訴瀏覽器——你希望執(zhí)行一個動畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫。該方法需要傳入一個回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會在瀏覽器下一次重繪之前執(zhí)行。
優(yōu)勢:
requestAnimationFrame 比起 setTimeout、setInterval的優(yōu)勢主要有兩點:
- requestAnimationFrame 是由系統(tǒng)來決定回調(diào)函數(shù)的執(zhí)行時機。它會把每一幀中的所有DOM操作集中起來,在一次重繪或回流中就完成,并且重繪或回流的時間間隔緊緊跟隨屏幕的刷新頻率,不會引起丟幀和卡頓。
- 使用setTimeout實現(xiàn)的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在后臺執(zhí)行動畫任務(wù),由于此時頁面處于不可見或不可用狀態(tài),刷新動畫是沒有意義的,完全是浪費CPU資源。而requestAnimationFrame則完全不同,當頁面處理未激活的狀態(tài)下,該頁面的屏幕刷新任務(wù)也會被系統(tǒng)暫停,因此跟著系統(tǒng)步伐走的requestAnimationFrame也會停止渲染,當頁面被激活時,動畫就從上次停留的地方繼續(xù)執(zhí)行,有效節(jié)省了CPU開銷。
用法:
window.requestAnimationFrame(callback)
回調(diào)參數(shù):
回調(diào)函數(shù)會被傳入DOMHighResTimeStamp參數(shù),DOMHighResTimeStamp指示當前被 requestAnimationFrame() 排序的回調(diào)函數(shù)被觸發(fā)的時間。
返回值:
返回整數(shù),即請求 ID ,是回調(diào)列表中唯一的標識。是個非零值,沒別的意義??梢詡鬟@個值給 window.cancelAnimationFrame() 以取消回調(diào)函數(shù)。
案例:
- 普通用法:
<div class="box"></div>
<button class="btn">click</button>
const box = document.querySelector('.box')
const btn = document.querySelector('.btn')
btn.addEventListener('click', () => {
let start
function handler(timestamp) {
if (!start) start = timestamp
const progress = timestamp - start
start = timestamp
console.log(progress)
if (box.offsetWidth < 100) {
box.style.width = `${box.offsetWidth + 1}px`
box.innerHTML = box.offsetWidth + '%'
requestAnimationFrame(handler)
}
}
requestAnimationFrame(handler)
})
- 取消回調(diào)
<div class="main">
<div class="content"></div>
<button class="button">click</button>
</div>
const content = document.querySelector('.content')
const button = document.querySelector('.button')
button.addEventListener('click', () => {
let rfaId
let left = 0
function handler() {
if (content.offsetWidth < 100) {
content.style.left = `${left++}px`
if (left >= 100) {
return cancelAnimationFrame(rfaId)
}
rfaId = requestAnimationFrame(handler)
}
}
rfaId = requestAnimationFrame(handler)
})
- 自定義raf時間
自定義raf時間通常是放慢刷新頻率,畢竟系統(tǒng)的16.7ms已經(jīng)是正常的,再快也沒什么意義。
<div class="main"></div>
<button class="btn">click</button>
const element = document.querySelector('.main')
const btn = document.querySelector('.btn')
btn.addEventListener('click', () => {
// 自定義的動畫時間差
const time = 50
let left = 0
let flag = true
// 當前執(zhí)行時間
let nowTime = Date.now()
// 每次動畫執(zhí)行結(jié)束的時間
let lastTime = Date.now()
// 執(zhí)行動畫
function handler() {
nowTime = Date.now()
if (nowTime - lastTime >= time) {
lastTime = nowTime
if (flag) {
if (left <= 100) {
element.style.left = `${left++}px`
} else {
flag = false
}
} else {
if (left >= 0) {
element.style.left = `${left--}px`
} else {
flag = true
}
}
}
requestAnimationFrame(handler)
}
requestAnimationFrame(handler)
})
當然,該API的用途不止設(shè)置動畫,還可以用于節(jié)流、防抖等。比如lodash關(guān)于節(jié)流、防抖的就是基于此API實現(xiàn)的。