假設有這樣一個動畫功能需求:把一個div的寬度從100px變化到200px。寫出來的代碼可能是這樣的:
復制代碼 代碼如下:
<div id="test1" style="width: 100px; height: 100px; background: blue; color: white;"></div>
function animate1(element, endValue, duration) {
var startTime = new Date(),
startValue = parseInt(element.style.width),
step = 1;
var timerId = setInterval(function() {
var nextValue = parseInt(element.style.width) + step;
element.style.width = nextValue + 'px';
if (nextValue >= endValue) {
clearInterval(timerId);
// 顯示動畫耗時
element.innerHTML = new Date - startTime;
}
}, duration / (endValue - startValue) * step);
}
animate1(document.getElementById('test1'), 200, 1000);
原理是每隔一定時間增加1px,一直到200px為止。然而,動畫結束後顯示的耗時卻不止1s(一般是1.5s左右)。究其原因,是因為setInterval並不能嚴格保證執行間隔。
有沒有更好的做法呢?下面先來看一道小學數學題:
復制代碼 代碼如下:
A樓和B樓相距100米,一個人勻速從A樓走到B樓,走了5分鐘到達目的地,問第3分鐘時他距離A樓多遠?
勻速運動中計算某個時刻路程的計算公式為:路程 * 當前時間 / 時間 。所以答案應為 100 * 3 / 5 = 60 。
這道題帶來的啟發是,某個時刻的路程是可以通過特定公式計算出來的。同理,動畫過程中某個時刻的值也可以通過公式計算出來,而不是累加得出:
復制代碼 代碼如下:
<div id="test2" style="width: 100px; height: 100px; background: red; color: white;"></div>
function animate2(element, endValue, duration) {
var startTime = new Date(),
startValue = parseInt(element.style.width);
var timerId = setInterval(function() {
var percentage = (new Date - startTime) / duration;
var stepValue = startValue + (endValue - startValue) * percentage;
element.style.width = stepValue + 'px';
if (percentage >= 1) {
clearInterval(timerId);
element.innerHTML = new Date - startTime;
}
}, 13);
}
animate2(document.getElementById('test2'), 200, 1000);
這樣改良之後,可以看到動畫執行耗時最多只會有10幾ms的誤差。但是問題還沒完全解決,在浏覽器開發工具中檢查test2元素可以發現,test2的最終寬度可能不止200px。仔細檢查animate2函數的代碼可以發現:
1.percentage的值可能大於1,可以通過Math.min限制最大值解決。
2.即使保證了percentage的值不大於1,只要endValue或startValue為小數,(endValue - startValue) * percentage的值也可能產生誤差,因為Javascript小數運算的精度不夠。其實我們要保證的只是最終值的准確性,所以在percentage為1的時候,直接使用endValue即可。
於是,animate2函數的代碼修改為:
復制代碼 代碼如下:
function animate2(element, endValue, duration) {
var startTime = new Date(),
startValue = parseInt(element.style.width);
var timerId = setInterval(function() {
// 保證百分率不大於1
var percentage = Math.min(1, (new Date - startTime) / duration);
var stepValue;
if (percentage >= 1) {
// 保證最終值的准確性
stepValue = endValue;
} else {
stepValue = startValue + (endValue - startValue) * percentage;
}
element.style.width = stepValue + 'px';
if (percentage >= 1) {
clearInterval(timerId);
element.innerHTML = new Date - startTime;
}
}, 13);
}
還有最後一個疑問:setInterval的間隔為何設為13ms?原因是當下顯示器的刷新率一般不超過75Hz(即每秒刷新75次,也就是每隔約13ms刷新一次),把間隔跟刷新率同步效果更好。