惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Last Watchdog
The Last Watchdog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
C
Check Point Blog
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
V
Vulnerabilities – Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
S
Security @ Cisco Blogs
IT之家
IT之家
T
The Exploit Database - CXSecurity.com
The GitHub Blog
The GitHub Blog
D
Docker
Engineering at Meta
Engineering at Meta
AWS News Blog
AWS News Blog
S
Security Affairs
U
Unit 42
P
Palo Alto Networks Blog
V
Visual Studio Blog
Y
Y Combinator Blog
D
DataBreaches.Net
Forbes - Security
Forbes - Security
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
Security Latest
Security Latest
aimingoo的专栏
aimingoo的专栏
Simon Willison's Weblog
Simon Willison's Weblog
A
Arctic Wolf
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hacker News: Front Page
博客园 - 司徒正美
博客园 - Franky
宝玉的分享
宝玉的分享
TaoSecurity Blog
TaoSecurity Blog
Latest news
Latest news
Scott Helme
Scott Helme
MongoDB | Blog
MongoDB | Blog
量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
P
Privacy International News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog

MARKSZのBlog

一次由 pnpm 小版本差异引发的 PicGo 2.5.3 发布事故 极空间虚拟机安装 Ubuntu 24.04 踩坑记录 | MARKSZのBlog PicGo 的签名与公证 | MARKSZのBlog 写在 PicGo 即将 8 周年之际 Vite 原理浅析 | MARKSZのBlog Typora 支持 PicGo 来上传图片了 | MARKSZのBlog 我的2019春招(暑期实习)记录 | MARKSZのBlog Electron-vue开发实战7——命令行调用与系统级别右键菜单的实现 | MARKSZのBlog Electron-vue开发实战6——开发插件系统之GUI部分 | MARKSZのBlog Electron-vue开发实战5——开发插件系统之CLI部分 | MARKSZのBlog 2018小结 | MARKSZのBlog 图床「神器」PicGo v2.0更新,插件系统终于来了 | MARKSZのBlog 一周一部好电影V【WEEK210 网络迷踪】 | MARKSZのBlog PicGo的star数破1000的心路历程 | MARKSZのBlog 小记VSCode插件amVim的改进以及插件开发 | MARKSZのBlog 基于Koa2开发微信二维码扫码支付相关流程 | MARKSZのBlog 【NOTE】观察者模式VS订阅发布模式 | MARKSZのBlog 【NOTE】进程-线程-协程 关系与区别 | MARKSZのBlog 基于Electron-vue的图床上传工具PicGo v1.5更新说明 | MARKSZのBlog
用setTimeout和clearTimeout简单实现setInterval与clearInterval | MARKSZのBlog
Molunerfinn · 2019-05-08 · via MARKSZのBlog

这个问题其实是前一段时间舍友的一道面试题。我觉得类似用reduce实现map、用xxx实现yyy的题目其实都挺有意思,考察融会贯通的本领。不过相比之下这道题可能更有实际意义。比如我们经常会用 setTimeout 来实现倒计时。下面来说说我对这个问题的思考。

简单版本

首先我们先用 setTimeout 实现一个简单版本的 setInterval

setInterval 需要不停循环调用,这让我们想到了递归调用自身:

1
2
3
4
5
6
7
8
9
const mySetInterval = (cb, time) => {
const fn = () => {
cb()
setTimeout(() => {
fn()
}, time)
}
setTimeout(fn, time)
}

让我们来写段代码测试一下:

1
2
3
mySetInterval(() => {
console.log(new Date())
}, 1000)

setTimeout-1

嗯,没啥问题,实现了我们想要的功能。。。等一下,怎么停下来?总不能执行了就不管了吧。。。

平时如果用到了 setInterval 的同学应该都知道 clearInterval 的存在(不然你怎么停下 interval 呢)。

clearInterval 的用法是 clearInterval(id)。而这个 idsetInterval的返回值,通过这个 id 值就能够清除指定的定时器。

1
2
3
4
5
const id = setInterval(() => {

}, 1000)

clearInterval(id)

不过你有没有想到 clearInterval 是如何实现的?回答这个问题之前,我们需要先实现 mySetInterval 的返回值。

mySetInterval的返回值

回到我们简单版本的 mySetInterval

1
2
3
4
5
6
7
8
9
const mySetInterval = (cb, time) => {
const fn = () => {
cb()
setTimeout(() => {
fn()
}, time)
}
setTimeout(fn, time)
}

现在它的返回值因为没有显示指定,所以是 undefined。因此第一步,我们先要返回一个 id 出去。

那么直接 return setTimeout(fn, time) 可以吗?因为我们知道 setTimeout 也会返回一个id,那么初步构想就是通过 setTimeout 返回的 id,然后调用 clearTimeout(id) 来实现我们的 myClearInterval

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mySetInterval = (cb, time) => {
const fn = () => {
cb()
setTimeout(() => {
fn()
}, time)
}
return setTimeout(fn, time)
}

const id = mySetInterval(() => {
console.log(new Date())
}, 1000)

setTimeout(() => {
clearTimeout(id)
}, 2000)

这显然是不行的。因为 mySetInterval 返回的 id 是第一个 setTimeoutid,然而2秒后,要 clearTimeout 时,递归执行的第二个、第三个 setTimeout 等等的 id 已经不再是第一个 id 了。因此此时无法清除。

所以我们需要每次执行 setTimeout的时候把新的 id 存下来。怎么存?我们应该会想到用闭包:

1
2
3
4
5
6
7
8
9
10
11
const mySetInterval = (cb, time) => {
let timeId
const fn = () => {
cb()
timeId = setTimeout(() => {
fn()
}, time)
}
timeId = setTimeout(fn, time)
return timeId
}

很不错,到这步我们已经能够将 timeId 进行更新了。不过还有问题,那就是执行 mySetInterval 的时候返回的 id 依然不是最新的 timeId。因为 timeId 只在 fn 内部被更新了,在外部并不知道它的更新。那有什么办法让 timeId 的更新也让外部知道呢?

有的,答案就是用全局变量。

1
2
3
4
5
6
7
8
9
10
11
let timeId 
const mySetInterval = (cb, time) => {
const fn = () => {
cb()
timeId = setTimeout(() => {
fn()
}, time)
}
timeId = setTimeout(fn, time)
return timeId
}

但是这样有个问题,由于 timeIdNumber类型,当我们这样使用的时候:

1
2
3
4
5
6
7
const id = mySetInterval(() => { 
console.log(new Date())
}, 1000)

setTimeout(() => {
clearTimeout(id)
}, 2000)

由于 idNumber 类型,我们拿到的是全局变量 timeId 的值拷贝而不是引用,所以上面那段代码依然无效。不过我们已经可以通过全局变量 timeId 来清除计时器了:

1
2
3
setTimeout(() => { 
clearTimeout(timeId)
}, 2000)

但是上面的实现,不仅与我们平时使用的 clearInterval 的用法有所出入,并且由于 timeId 是一个 Number 类型的变量,导致同一时刻全局只能有一个 mySetIntervalid 存在,也即无法做到清除多个 mySetInterval 的计时器。

所以我们需要一种类型,既能支持多个 timeId 存在,又能实现 mySetInterval 返回的 id 能够被我们的 myClearInterval 使用。你应该能想到,我们要用一个全局的 Object 来做。

修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let timeMap = {}
let id = 0
const mySetInterval = (cb, time) => {
let timeId = id
id++
let fn = () => {
cb()
timeMap[timeId] = setTimeout(() => {
fn()
}, time)
}
timeMap[timeId] = setTimeout(fn, time)
return timeId
}

我们的 mySetInterval 依然返回了一个 id 值。只不过这个 id 值是全局变量 timeMap 里的一个键的内容。

我们每次更新 setTimeoutid 并不是去更新 timeId,相应的,我们去更新 timeMap[timeId] 里的值。

这样实现后,我们调用 mySetInterval 虽然获取到的 timeId 是不变的,但是我们通过 timeMap[timeId] 获取到的真正的 setTimeoutid 值是会一直更新的。

另外为了保证 timeId 的唯一性,在这里我简单用了一个自增的全局变量 id 来保证唯一。

好了,id 值有了,剩下的就是 myClearInterval 的实现了。

myClearInterval实现

由于我们的 mySetInterval 返回的 timeId 并不是真正的 setTimeout 返回的 id ,所以并不能简单地通过 clearTimeout(timeId) 来清除计时器。

不过其实原理也是很类似的,我们只要能拿到真正的 id 就行了:

1
2
3
4
const myClearInterval = (id) => {
clearTimeout(timeMap[id])
delete timeMap[id]
}

测试一下:

没毛病~

至此我们就用 setTimeoutclearTimeout 简单实现了 setIntervalclearInterval。当然本文说的是简单实现,毕竟还有一些东西没有完成,比如setTimeoutargs 参数、Node和浏览器端的 setTimeout 差异等等。也只是一个抛砖引玉,重点在一步步如何实现。感谢阅读~

Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.