定时器
使用 RequestAnimationFrame 实现定时器
浏览器默认提供的 setTimeout 和 setInterval 的问题
首先在针对浏览器端的默认实现中,setTimeout 和 setInterval 的定时是 不准确的,因为我们知道 js 是单线程的,如果前面的代码耗费了较长的时间,那么会导致后面的定时器不能按时执行。此外 setInterval 会带来性能上的问题,比如存在执行累积的问题等等。 可以使用 requestAnimationFrame 来实现定时器的要求,首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6ms 内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题。
requestAnimationFrame 简单介绍
requestAnimationFrame 的语法很简单:window.requestAnimationFrame(callback)
。callback 为一个指定函数的参数,该函数在下次重新绘制动画时调用
setTimeout 实现
重新绘制的时间即 16.6ms 执行。我们可以通过递归调用来达到定时器的效果
js
function _setTimeout(fn, timeout) {
let timer;
let startTime = Date.now();
const loop = () => {
timer = window.requestAnimationFrame(loop);
if (Date.now() - startTime >= timeout) {
fn.call(this, timer);
window.cancelAnimationFrame(timer);
}
};
window.requestAnimationFrame(loop);
}
这里需要先定义一个开始时间 startTime,然后定义一个递归函数 loop,每次先递归调用自己获取最新的 timer,这样才能够保证可以取消掉,然后判断当前时间减去开始时间是否大于自己设定的值,如果大则回调并取消定时器即可,否则因为我们已经在一开始回调自己了,所以会在 16.6ms 后再次执行并判断,直到满足条件为止。
setInterval 实现
js
function _setInterval(fn, interval) {
let timer;
let startTime = Date.now();
const loop = () => {
timer = window.requestAnimationFrame(loop);
if (Date.now() - startTime >= interval) {
fn.call(this, timer);
startTime = Date.now();
}
};
timer = window.RequestAnimationFrame(loop);
return timer;
}
setTimeout 实现 setInterval
不完整实现:
js
const _setInterval = (fn, interval) => {
let timer;
const loop = () => {
timer = setTimeout(() => {
// timeout 时间之后会执行真正的函数 fn
fn();
// 同时再次调用 interval 本身
loop();
}, interval);
};
// 开始执行
loop();
// 返回用于关闭定时器的函数
return () => clearTimeout(timer);
};
let cancelInterval = _setInterval(() => {
console.log(1);
}, 300);
setTimeout(() => {
cancelInterval();
console.log("1s之后关闭定时器");
}, 1000);
另一种方案:
js
function _setInterval(handler, timeout, ...args) {
// 判断是否为浏览器环境
let isBrowser = typeof window !== "undefined";
if (isBrowser && this !== window) {
throw new TypeError("Illegal invocation");
}
let timer = {};
if (isBrowser) {
// 浏览器上的处理
timer = {
value: -1,
valueOf() {
return this.value;
},
};
const callback = () => {
timer.value = setTimeout(callback, timeout);
handler.apply(this, args);
};
timer.value = setTimeout(callback, timeout);
} else {
// nodejs 的处理
const callback = () => {
Object.assign(timer, setTimeout(callback, timeout));
handler.apply(this, args);
};
Object.assign(timer, setTimeout(callback, timeout));
}
return timer;
}
setInterval 模拟 setTimeout
思路:setTimeout 的特性是在指定的时间内只执行一次,我们只要在 setInterval 内部执行 callback 之后,把定时器关掉即可。
js
const _setTimeout = (fn, timeout) => {
let timer = null;
timer = setInterval(() => {
// 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了
clearInterval(timer);
fn();
}, timeout);
// 返回用于关闭定时器的方法
return () => clearInterval(timer);
};
let cancelTimeout = _setTimeout(() => {
console.log("1s 后打印出 1");
}, 1000);