记录一个不常见的API - 请求动画帧
requestAnimationFrame 用法: handlerId = requestAnimationFrame(callback);
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
- 传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘前执行。
- 返回值
handlerId为浏览器定义的、大于0的整数,唯一标识了该回调函数在列表中位置,用于取消回调函数,有点类似setInterval。 - 同时:
requestAnimationFrame还是 react 官方团队为了兼容浏览器差异实现requestIdleCallback(利用浏览器空闲时间来计算,使fiber可以基于优先级计算并获得极高性能原因之一)而设计的ployfill原理之一。
浏览器在执行requestAnimationFrame 发生了什么呢 ?
- 首先要判断
document.hidden属性是否为true,即页面处于可见状态下才会执行; - 浏览器清空上一轮的动画函数;
- 方法返回的
handlerId值会和动画函数callback,以<handlerId , callback>进入到动画帧请求回调函数列; - 浏览器会遍历动画帧请求回调函数列表,根据
handlerId的值大小,依次去执行相应的动画函数。 - 注意:
requestAnimationFrame()排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为 1ms(1000μs)。
cancelAnimationFrame 用于取消动画帧函数 用法: cancelAnimationFrame(handlerId);
使用示例
const element = document.getElementById('some-element-you-want-to-animate');
let start, previousTimeStamp;
let done = false
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimeStamp !== timestamp) {
// 这里使用 `Math.min()` 确保元素刚好停在 200px 的位置。
const count = Math.min(0.1 * elapsed, 200);
element.style.transform = 'translateX(' + count + 'px)';
if (count === 200) done = true;
}
if (elapsed < 2000) { // 在两秒后停止动画
previousTimeStamp = timestamp;
if (!done) {
window.requestAnimationFrame(step);
}
}
}
window.requestAnimationFrame(step);
还可以用来做进度条
<div id="myDiv" style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div>
<button id="btn">run</button>
<script>
var timer;
btn.onclick = function(){
myDiv.style.width = '0';
cancelAnimationFrame(timer);
timer = requestAnimationFrame(function fn(){
if(parseInt(myDiv.style.width) < 500){
myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px';
myDiv.innerHTML = parseInt(myDiv.style.width)/5 + '%';
timer = requestAnimationFrame(fn);
}else{
cancelAnimationFrame(timer);
}
});
}
</script>
那么这样的API除了可以控制动画外,还可以用来做什么呢?
React ahooks 中 给了我们一个好的启发: React ahooks 中对 setRafState 的封装通过配合useCallback 对useState 做了优化。
const useRafState<S>(initialState?: S | (() => S)) => {
const ref = useRef(0);
const [state, setState] = useState(initialState);
useEffect(() => {
return {
cancelAnimationFrame(ref.current);
}
}, [])
const setRefState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(ref.current);
//只在 requestAnimationFrame callback 时更新 state,一般用于性能优化。
ref.current = requestAnimationFrame(() => {
setState(value);
}, []);
}
return [state, setRafState] as const;
}
export default useRafState;