前言
在面试时被问到这道经典的大厂老考题,一时半会是不是脑子懵懵的不知如何实现,今天我们来从头到尾解析这道题,把它稳稳拿下。
setTimeout
在了解定时器概念前我们首先要了解JS是单线程的,也就是说在v8引擎执行时,只会有一个主线程,不能同时执行多个程序。
setTimeout是异步执行的计时器,异步编程是一种允许程序在等待某个操作完成前先执行其他任务的编程模式,与之相对的就是我们正常的同步编程,其中每一个操作都必须按顺序一个接一个完成,下一个操作只有在前一个操作完成后才能开始。
很关键的一点:可能有人会想我等待它执行完再执行下面的程序不就行了吗?这种想法是错误的,因为如果卡住等待,页面都将会卡住,也不能响应其他事情以及满足和用户的交互,比如说鼠标点击,那你想想这样用户的体验该有多差。
setTimeout的基本语法:
setTimeout(func, delay, [param1, param2, ...]);
func中存放需要执行的函数,这个函数我们称为回调函数callback,回调函数就是一个函数作为参数传递给另一个函数,并在指定事件后执行,它会放在一个专有的地方(event loop)等待主线完成,这个我们后面再细讲。delay表示延迟的时间,通常用毫秒来表示1000ms = 1s,这两个参数是必须写的,后面两个则是可选的传递给回调函数的参数。我们的setTimeout本身也会返回一个值,那就是定时器的ID,每一个定时器都有自己唯一的id。我们来看一个小小的例子:
setTimeout(() => {
console.log("1");
}, 1000);
console.log("2");
这就是一个简单的定时器实现,我们规定了在1秒钟后才会执行,所以它会先执行下面的函数,先输出2,1秒后再输出1。
诶?那我们会想,真的假的,我让他在1秒后执行它就一定会在一秒后执行吗?
答案当然是不会的,它最重要的是确保回调函数不会在指定时间之前被执行,而在指定时间之后它也必须等待主线程正在进行的任务完成(例如,处理用户输入、执行其他 JavaScript 代码等)之后再进行,并且浏览器本身会对定时器设定1~5ms的延迟,防止过多的高频率定时器消耗过多的系统资源。
setInterval
setInterval也是一个定时器,与setTimeout不同的是,它的功能是按照指定的时间间隔循环执行传入的回调函数。
我们在浏览器打开看看效果:
此时已经循环了9次,那么这时候面临一个问题,总不能他喵让他一直循环下去吧,那我们此时就需要一个功能来让他停下来,此时就需要用到我们之前说的每个定时器有唯一的ID,我们只需要获取它的id然后用我们JavaScript中内置的停止定时器的方法clearTimeout为其添加一个点击事件就可以了:
button id="btn1">关闭定时器button>
script>
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click',function(){
clearInterval(timeout)
})
const timeout = setInterval(() => {
console.log('a')
},1000)
script>
在运行10次后点击按钮他就停止了。
小结
setTimeout 是异步执行的定时器,会在规定时间之后执行它的回调函数返回值是定时器ID,也有可能不能准确执行,因为js是单线程的,它要在主线程完成之后才会有执行的机会。
面试题实现
有以上的知识之后,我们就可以开始手撕这道面试题了,那我们应该思考这道题目的要求和限制,既然是用setTimeout实现一个setInterval,那么前者我们能使用而后者是不可以的,那么关键就是就如何让setTimeout实现循环操作呢?
答案就是递归。
script>
function coustomSetInterval(callback,time){
let intervalId = null;
function loop(){
intervalId = setTimeout(() => {
callback();
loop();
},time)
}
loop();
return () => clearTimeout(intervalId)
}
const interval = coustomSetInterval(() => {
console.log('想你了')
},1000)
setTimeout(() => {
interval()
},5000)
script>
我们定义了一个函数coustomSetInterval来传入我们的回调函数和时间,并在内部先将intervalId设置为null方便后面使用它,然后将setTimeout放入函数loop()内部确保它能执行递归,所以在callback()函数执行后再次执行loop()就利用setTimeout实现了setInterval,所以我们只需要在函数定义完成后的外部执行一次loop()函数它就会一直递归,并且我们返回了一个箭头函数clearTimeout(intervalId) 这是coustomSetInterval的返回值,用来我们将递归停止。
于是在外部我们调用coustomSetInterval函数后将它的返回值赋值给了interval并在最后再添加了一个定时器用于在5秒后执行interval()也就是返回的箭头函数,至此一个完整的利用setTimeout实现setInterval就完成了。让我们来看看它的效果:
在执行4次之后就停止了。
小结
定时器作为面试时的常考题,需要我们熟练掌握,如果有问题欢迎评论区交流,希望能对你有帮助。