Skip to content

这个对象从名字上来说就能看出来是一个用来做动画的东西,首先我们先用 JS 来实现一个动画看一看

js
const box = document.getElementsByClassName("box")[0];
const boxPosition = {
	left: parseFloat(window.getComputedStyle(box).left),
	top: parseFloat(window.getComputedStyle(box).top)
};
const proxBox = new Proxy(boxPosition, {
	set(target, key, value) {
		if (key === "left" || key === "top")
			box.style[key] = value + "px";
		Reflect.set(target, key, value);
	}
});
setInterval(() => {
	proxBox.left += 1;
	proxBox.top += 1;
}, 16);

这里是用代理实现的,如果读者没有接触过 ES 6,可以使用下面更简单的方式

js
const box = document.getElementsByClassName("box")[0];
box.style.left = window.getComputedStyle(box).left;
box.style.top = window.getComputedStyle(box).top;
setInterval(() => {
	box.style.left = parseFloat(box.style.left) + 1 + "px";
	box.style.top = parseFloat(box.style.top) + 1 + "px";
}, 16);

接下来看效果:

![recording 10](../04.CSS3/assets/recording 10.gif)

这里解释一下为什么定时器设置的是 16 毫秒,其实很简单,浏览器的刷新频率一般是 60 帧,也就是一秒钟 60 张图片,而 1000/60=16.6666666667,也就是说 16 帧刚好能契合上浏览器的刷新率。

但是我们其实很清楚,但是浏览器计算也是需要时间的,我们无法保证渲染的时候已经计算好了,所以也很难实现完美的契合浏览器刷新率。

此时为了契合浏览器刷新率,更好实现 JS 动画,就可以使用 window.requestAnimationFrame() 方法来实现,该方法就是专门用来实现动画的,这个方法传入一个回调函数,当渲染帧到达的之前会先调用该方法,该方法结束后才会进行渲染,注意,该方法是一次性的,如果我要继续更新下一帧率的动画,需要使用递归。

既然涉及到动画,这里肯定涉及到时间,这个回调函数传入的参数就是一个时间,这个时间就是这个函数执行时刻的时间。并且这个 requestAnimationFrame 函数会返回一个 ID,我们可以使用 cancelAnimationFrame() 传入一个 id 从而取消这个渲染帧函数

js
    /**
     * 获取页面中第一个class为"box"的元素,并初始化boxPosition对象用于记录box的初始位置
     * @returns {HTMLElement} 页面中第一个class为"box"的元素
     */
    const box = document.getElementsByClassName("box")[0];

    /**
     * 初始化boxPosition对象,存储元素的初始left和top位置
     * @type {{left: number, top: number}}
     */
    const boxPosition = {
        left: parseFloat(window.getComputedStyle(box).left),
        top: parseFloat(window.getComputedStyle(box).top)
    };
    /**
     * 创建一个Proxy代理对象proxBox,拦截对boxPosition.left和boxPosition.top的修改并同步到box元素的样式中
     * @type {*}
     */
    const proxBox = new Proxy(boxPosition, {
        set(target, key, value) {
            if (key === "left" || key === "top") {
                box.style[key] = value + "px";
            }
            Reflect.set(target, key, value);
        }
    });
    /**
     * 初始化动画相关变量
     */
    let lastTime = 0; // 上一帧时间
    let isAnimate = false; // 是否正在执行动画
    let rid = 0; // 动画请求ID
    /**
     * 实现动画逻辑的函数
     * @param {number} time 当前时间戳(毫秒)
     */
    function animate(time) {
        if (!isAnimate) {
            lastTime = time;
            isAnimate = true;
        } else {
            // 计算自上一帧以来的时间差
            const disTime = time - lastTime;
            lastTime = time;
            // 根据时间差计算移动距离,假设每毫秒移动0.1像素
            const s = disTime * 0.1;
            // 更新box的位置
            proxBox.left += s;
            proxBox.top += s;
        }
        rid = requestAnimationFrame(animate);
    }
    // 监听box元素的点击事件,启动或停止动画
    box.onclick = function () {
        if (!isAnimate) {
            rid = requestAnimationFrame(animate);
        } else {
            cancelAnimationFrame(rid);
            isAnimate = false;
            rid = 0;
        }
    }

这样实现出来的动画会更加细腻,这种方式的性能也比定时器动画要更好,当然,我们要使用动画最丝滑的还是 CSS 3

这里我们可以使用 requestAnimationFrame 封装一个动画函数

js
function animation(from, to, duration, fn) {
	//在duration的时间内,从from变到to,每次变化调用fn,传入当前数值
	const startTime = Date.now();//获取当前时间戳
	const speed = (to - from) / duration;//获取速度
	let rid;
	function run() {
		const time = Date.now() - startTime;//获取运行该函数时候的相对时间
		const currentValue = from + speed * time;//获取当前数值
		if (currentValue >= to) {//如果当前数值达到标准
			cancelAnimationFrame(rid);//取消
			fn(to);
			return;//返回
		}
		fn(currentValue);
		rid = requestAnimationFrame(run);
	}
	requestAnimationFrame(run);
}

const box = document.querySelector('.box');
animation(0, 200, 2000, (v) => {
	box.style.left = v + 'px';
});

以笔记为镜,可以知得失