资讯动态

自学前端第8天:用JavaScript手写极简番茄钟

发布时间:2026/10/18 12:19:24 来源:尧图企业网站定制
学前端这件事我今天刚好坚持到第8天。前一周都在跟HTML和CSS较劲看得多写得少今天决定换一种方式不再照着教程敲示例而是自己动手做一个小工具。选来选去最后做了一个极简番茄钟。这个选择有点偶然但做完之后发现它几乎把我前七天学过的DOM操作、事件绑定、定时器、数组和对象方法全串起来了算是一次很完整的阶段性自测。如果你也在自学前端正处于“语法看懂了但不知道怎么写项目”的阶段这篇学习记录应该能给你一点参考。我尽量把每个决策背后的原因、代码为什么这样写、踩过哪几个坑都记下来不光是贴代码更多是分析思路。毕竟学习记录的价值不在于“又写了一个Demo”而在于这个Demo是怎么从脑子里一步步变成屏幕上的东西的。1. 第八天从“看懂代码”到“动手写代码”的转折1.1 前面七天我都干了啥先快速回顾一下前七天的进度方便你判断我现在的水平处在哪个位置。这样后面看我写代码时哪里卡顿、哪里出错你也能大概估算出自己会不会遇到类似的问题。天数学习内容掌握程度Day1HTML基础标签、文档结构能写静态页面但不熟语义化Day2表单、列表、表格、语义化标签了解了header/footer/main的用处Day3CSS选择器、盒模型、浮动会改样式但布局容易乱Day4Flexbox和Grid布局能实现常见的两栏/三栏布局Day5JavaScript变量、类型、运算符、函数能写出简单的计算逻辑Day6DOM操作、事件监听基础会选中元素、改内容、绑定点击Day7数组和对象的基本方法会用map/filter/forEach处理数据严格来说直到Day7我还只是在“积累零件”没有真正组装过什么。Day8开始前我问了自己一个问题如果让我不看教程独立做一个能交互的小页面我能做出来吗答案有点心虚。所以今天的目标就变成了——关掉所有教程的示例代码自己设计一个完整的小功能。1.2 为什么挑“番茄钟”当作第一个小项目选番茄钟的原因是它麻雀虽小五脏俱全。表面上只有“开始、暂停、重置”三个按钮但要做到真正能用它需要处理状态切换、时间计算、定时器管理、按钮防连点甚至还要考虑浏览器切到后台之后定时器不准的问题。这些可不是“照着书上写两句”能遇到的。当时也有想过做待办清单、计算器之类的。待办清单的核心是数据的增删改查涉及localStorage对第八天的我来说还是有点偏。计算器主要考验事件处理和运算符优先级又太偏向逻辑题。番茄钟更像一个真实产品有时间显示、有状态流转、有异常边界难度正好卡在“跳一跳够得着”的位置。从学习效率来说我也推荐你选一个“需要管理状态”的小项目来练手而不是单纯做静态页面。因为状态管理几乎是所有前端应用的灵魂番茄钟就是状态管理的最小闭环未开始、计时中、已暂停、已结束四种状态来回切换每一步都逼着你把逻辑理清楚。2. 动手前先弄懂的两个核心概念定时器与状态2.1 setInterval不是“精确时钟”我一开始对定时器的理解特别天真觉得setInterval(fn, 1000)就是每隔1000毫秒执行一次那我每执行一次就把剩余秒数减一不就是一个倒计时了实际写完才发现这个思路有一个很大的坑setInterval只承诺“每隔一段时间把任务丢进队列”不承诺任务按精确的时间间隔执行。如果主线程很忙或者浏览器标签页被切到后台很多浏览器会主动降低定时器频率甚至把回调暂停。我实测了一下把页面切到后台几十秒再切回来倒计时确实还在走但走的秒数明显少于实际经过的时间。这里有一个很直观的类比setInterval像一个每隔一秒就发射一颗子弹的机枪但它瞄准的是“当前这一时刻的剩余时间”而不是“下一次该显示的时间”。只要中间有任何一发射偏了之后所有弹着点都会跟着偏。真正可靠的倒计时不能靠“每秒减一”而要靠时间戳来算。2.2 避免累计误差用时间戳算剩余时间正确做法是记录一个目标结束时间endTime每次刷新显示时用Math.round((endTime - Date.now()) / 1000)来算出剩余秒数。这样不管定时器回调是准时来、迟到来、还是被浏览器卡了半分钟只要它还能执行一次就能立刻拉回正确的时间。let endTime null; let remainingSeconds 25 * 60; function tick() { remainingSeconds Math.round((endTime - Date.now()) / 1000); if (remainingSeconds 0) { remainingSeconds 0; stopTimer(); alert(时间到); } updateDisplay(); }这个思路当时把我点醒了倒计时的本质不是“数字一点点变小”而是“当前时刻与结束时刻之间的距离”。理解了这一点后面处理暂停、重置都会清晰很多。2.3 状态变量一个按钮背后可能藏着四种状态另一个容易忽视的点是状态管理。番茄钟的“开始”按钮点击之后应该变成“暂停”再点击又要变回“继续”同时“重置”按钮始终可用。如果只用几个分散的变量去记录“现在是不是在计时”很容易出现逻辑漏洞比如连续点击开始按钮导致定时器叠加。我在做之前先草草画了一个状态表状态开始按钮文本定时器状态可执行操作idle未开始开始无开始、重置running计时中暂停运行中暂停、重置paused已暂停继续暂停继续、重置finished已结束开始无重置有了这个表后面写代码的时候就不会“写到哪算哪”而是每一句都在为状态迁移服务。这是Day8最值钱的一个认知交互功能开发之前先定义清楚状态再写代码。3. 番茄钟的代码实现从界面到逻辑3.1 页面结构三按钮一显示先写一个尽量简单的结构。我把三个按钮放在一起时间显示在最上面。番茄钟不需要花哨样式能用就行注意力放在逻辑上。div classtimer-app div idtimer25:00/div button idstartBtn开始/button button idresetBtn重置/button /div这里我只做了两个按钮把暂停功能合并到“开始/暂停”同一个按钮上。这样可以减少一个按钮的状态同步问题对新手来说更友好。CSS部分就用Flexbox居中字体调大一点没有写得太复杂。3.2 时间显示的初始化与格式化时间显示的格式是MM:SS如果少于10秒秒数前面要补零。这里我用了一次字符串的padStart方法这也是前面学String方法时的实际应用。const timerDisplay document.getElementById(timer); const startBtn document.getElementById(startBtn); const resetBtn document.getElementById(resetBtn); const totalSeconds 25 * 60; let remainingSeconds totalSeconds; let timerId null; let endTime null; function formatTime(seconds) { const minutes String(Math.floor(seconds / 60)).padStart(2, 0); const secs String(seconds % 60).padStart(2, 0); return ${minutes}:${secs}; } function updateDisplay() { timerDisplay.textContent formatTime(remainingSeconds); } updateDisplay();padStart(2, 0)的意思是如果这个字符串不足2个字符就在前面补0。比如5秒会变成0525分钟会变成25。这个API我前几天刚学今天用上之后记忆特别牢。3.3 开始、暂停、重置同一个按钮怎么切换语义接下来是最核心的逻辑。我把开始和暂停放在同一个按钮里通过判断当前是否有定时器在运行来决定点击后的行为。function startTimer() { if (timerId ! null) { return; // 已经在计时了不能再开新的定时器 } endTime Date.now() remainingSeconds * 1000; timerId setInterval(tick, 250); startBtn.textContent 暂停; } function pauseTimer() { if (timerId null) { return; } clearInterval(timerId); timerId null; remainingSeconds Math.round((endTime - Date.now()) / 1000); startBtn.textContent 继续; } function resetTimer() { clearInterval(timerId); timerId null; remainingSeconds totalSeconds; updateDisplay(); startBtn.textContent 开始; } startBtn.addEventListener(click, function () { if (timerId null) { startTimer(); } else { pauseTimer(); } }); resetBtn.addEventListener(click, resetTimer);这里的setInterval间隔我用了250毫秒而不是1000毫秒。原因很简单1000毫秒的定时器在“边角时间”上误差比较明显可能在剩余时间刚好是00:00之前就显示成00:00。250毫秒刷新能让显示更平滑配合时间戳计算误差基本小于0.25秒。对初学者来说这个300行以内的完整逻辑刚刚好。3.4 完整代码放在一起什么样把所有代码合并到一个HTML文件里直接双击打开就能用。下面是我今天最终跑起来的版本结构不算优雅但功能完整作为Day8的成果我很满意。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title极简番茄钟/title style body { display: flex; justify-content: center; align-items: center; height: 100vh; font-family: sans-serif; } .timer-app { text-align: center; } #timer { font-size: 72px; margin-bottom: 20px; } button { font-size: 18px; padding: 10px 24px; margin: 0 8px; } /style /head body div classtimer-app div idtimer25:00/div button idstartBtn开始/button button idresetBtn重置/button /div script // 省略完整的JS逻辑核心代码如上 /script /body /html写完之后测试了几轮25分钟没有耐心等但把时间改成1分钟甚至10秒来测逻辑都能跑通。今天的主要目的不是做一个多完整的成品而是验证自己能不能独立走通“设计—编码—测试”的流程。4. 踩坑记录两个Bug和它们的排查过程4.1 Bug1开始按钮连点定时器越走越快第一个Bug来得很快我满怀信心地连点了一下“开始”发现倒计时数字像开了倍速一样哗哗往下掉。那一刻我意识到自己犯了新手最容易犯的错没有拦住重复开启定时器。我一开始的startTimer函数长这样function startTimer() { endTime Date.now() remainingSeconds * 1000; timerId setInterval(tick, 250); }第一次点击会创建一个定时器second time点击又会创建一个新的但旧的还在跑。两个定时器同时执行tick而tick里的逻辑是让剩余秒数减少所以速度瞬间翻倍。再点一次就更离谱三倍速倒计时整个页面的节奏全乱了。排查过程其实很快我在tick里打印了一下timerId发现每次点击后timerId都在变但旧定时器仍然在后台运行。解法很简单就是前面代码里已经写进去的判断如果timerId不是null直接return不允许再次创建定时器。if (timerId ! null) { return; }这也让我明白了一个原则任何“一次性操作”都必须在入口处设置一个守卫条件否则用户的操作往往会把程序推向不可控的边界。4.2 Bug2切到后台再回来时间显示不对了第二个Bug没有第一个那么直接。我把番茄钟挂在后台去刷了一会儿手机回来发现计时器显示的时间明显比实际经过的要多好像它中间自己“睡了一会儿”。这个问题一开始我完全没有头绪因为代码看起来完全符合逻辑每250毫秒执行一次tick剩余时间不断减少。后来查了资料才明白浏览器对后台标签页的定时器有节流机制。切到后台之后setInterval的间隔可能被放大到1分钟甚至更久回调执行次数大幅下降但我的逻辑是“每次执行就把剩余时间算一遍”所以显示出来的剩余时间就停在了某一个值附近落后于真实时间。解决方式就是把“每秒减一”的逻辑彻底替换为“看时间戳”。因为不管setInterval被延迟多久它执行的那一瞬间Date.now()返回的一定是真实时间。用真实时间减去结束时间结果永远是正确的剩余时间。这是整个番茄钟里最值得记住的一个经验凡是和时间有关的功能一律以时间戳为准不要依赖计数器的累加。4.3 按钮文案切换一个小细节带来的体验提升写最后一个状态切换时我发现如果每次点击之后按钮文字都不变用户很难判断当前是“暂停中”还是“计时中”。所以我给开始按钮做了三个状态文案未开始时显示“开始”计时中显示“暂停”暂停后显示“继续”。这样一眼就能看出当前处于什么阶段。这里有一个小插曲第一次做状态切换时我在pauseTimer里把按钮文字改成了“暂停”但忘了把点击事件的判断逻辑改掉。结果出现了“按钮显示暂停但点击后却重新开始计时”的尴尬情况。后来我把“当前按钮文案”和“timerId是否为空”绑定在同一条判断链路里才彻底理顺。UI状态和逻辑状态必须保持同步这个教训值得记下来。5. 今天留下的小思考明天可以给番茄钟加什么5.1 加一个“今日完成次数”统计番茄钟做完之后我试着在脑子里延伸了一下如果明天继续做这个项目我要加的第一个功能是“完成次数统计”。每完成一个番茄钟数字加一并且刷新页面之后依然保留。这就必然会用到localStorage。localStorage的用法并不复杂存储的是字符串读出来之后需要转成数字或对象。我虽然没有在Day8实现它但已经知道大概的步骤每次tick达到0秒时取出本地存储里的count加一再写回去。这个思路今天先记录下来明天可以直接上手试。如果再把思路放大一点还可以把每次完成的开始时间和结束时间存成数组用数组的map方法渲染成一个历史记录列表。这样我的番茄钟就不再是“用完就关”的小页面而是一个有持久化能力的小工具。学习周期就是这样先跑通最小闭环再一层层往上加需求。5.2 关于学习的节奏我想说两句Day8是个很微妙的节点既不像第一周那样充满新鲜感也不像一个月后那样能随手写出各种组件。今天最大的感受是如果只看教程我可能会在第8天继续看第10个或者第20个教学视频一直处在“我会了”的幻觉里。但真到了自己动手写哪怕是这样一个小番茄钟也逼着我面对了很多“教程里不会写”的问题定时器叠加、后台节流、按钮状态同步。也许这就是所谓的“输出倒逼输入”。做题和写项目是完全两回事尤其写项目的过程中你会发现自己以为懂了的某些语法其实只是“眼熟”。当代码报错时真正的理解才刚开始。明天我打算继续沿着这个番茄钟的思路往下走完成次数统计、本地存储、音效提醒一步一步补上。也欢迎你按照同样的方式选一个自己感兴趣的小项目把第八天当成第一个“实战日”。我猜等你做完之后也会有一种很踏实的感觉原来那些零散的语法真的能凑出一个能跑的东西。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑