在“写个时钟(行为篇)”中,我们通过 JavaScript 动态创建的时针、分针和秒针,并直接在 JavaScript 中通过控制行内样式的 transform 属性,设置 rotate 的值,实现指针的旋转。这样的方式,对于 DOM 的控制消耗较大,若放到大项目中,对项目性能具有一定的影响,也让 JavaScript 代码较多,降低了可读性。
而且,设置实时时间的 setRealTime 函数,我们使用了递归调用,每隔一秒钟调用一次,那么每隔一秒钟就会运行整个函数一次,感觉简直是资源的浪费。
仔细想想,都已经是 4202 年了,感觉还是使用 JavaScript 大量控制 DOM 的方式,怎么看都有点点过时了。
曾经,听一位前端大神(尊重隐私,不透露名字)说过一句话:
那么,秉承这一原则,我们对时钟页面作更加前卫的进阶处理吧!
一、时钟指针静态化
咱们前面是使用 JavaScript 动态生成的三个指针元素,通过 createElement 方法创建 DOM 对性能消耗较大,而且感觉不是必须的。于是我们将其直接写到 html 中:
<div class="dial">
<div class="pointer hour"></div>
<div class="pointer minute"></div>
<div class="pointer second"></div>
</div>
如此一来,原本 JavaScript 中占有 16 行的创建指针的 createPointer 函数就可以删除了。
创建刻度的 createScale 函数不能删除,因为刻度有足足 60 个之多,直接在 html 中写 60 个 span 标签,还要每 5 个就给其加一个值为 bold 的 class 属性。相比之下,感觉还是在 JavaScript 中使用循环生成,代码显得更加简洁一些,逻辑上也更加优雅一些。
删除了创建指针的 createPointer 函数,对应的创建刻度的 createScale 函数的最后一行,就不能再调用创建指针的 createPointer 函数,而是应该直接调用设置实时时间的 setRealTime 函数:
/**
* @description 创建刻度
*/
const createScale = () => {
const oDiv = document.querySelector('.dial'); // 获取钟表盘面
for (let i = 0; i < 60; i++) {
const oSpan = document.createElement('span'); // 创建刻度元素
oSpan.style.transform = `rotate(${6 * i}deg)`; // 设置旋转刻度属性
if (i % 5 === 0) {
oSpan.className = 'bold'; // 每 5 个刻度,让其显示突出一些
}
oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
}
setRealTime(); // 设置实时时间
}
注意,设置实时时间的 setRealTime 函数,之前我们是把三个指针元素当作参数传入的,现在不再需要传入参数了。
二、重构设置实时时间函数
还是和“行为篇”一样的思路先构建一个设置实时时间的 setRealTime 函数,并获取到会用到的小时、分钟和秒:
/**
* @description 设置实时时间
*/
const setRealTime = () => {
const now = new Date(); // 获取当前时间
const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
}
我们知道,在 CSS 中,控制一个元素的旋转,都是通过 transform 属性中的 rotate 方法实现的,其语法为:transform: rotate(αdeg); 。
α 为旋转的度数,单位 deg 代表的是角度制。
说白了,就是以 transform-origin 定义的坐标位置为极坐标的原点(未定义该属性,默认为该元素的几何中心,即:transform-origin: 50% 50% 0;),顺时针旋转 α 个度数。
既然拿到了实时时间中的小时、分钟和秒,那么我们就可以根据性质,让其转行成度数,并设置到元素中。
document.documentElement.style.setProperty('--hour', `${30 * hour}deg`); // 根据当前小时设置小时指针的旋转角度
document.documentElement.style.setProperty('--minute', `${6 * minute}deg`); // 根据当前分钟设置分钟指针的旋转角度
document.documentElement.style.setProperty('--second', `${6 * second}deg`); // 根据当前秒设置秒指针的旋转角度
耶???这是嘛呀???
没错,考虑到我们是把三个指针元素直接写在 html 中的,从心理上来说,我是不愿意再次从 JavaScript 中获取这三个元素的。既然如此,就只好把在 JavaScript 中计算出来的值传到 CSS 中进行处理。于是,CSS4 中的变量登场了:
.dial > .hour {
top: 125px;
width: 10px;
height: 200px;
transform-origin: center 175px;
transform: rotate(var(--hour)) ;
}
.dial > .minute {
top: 25px;
width: 7.5px;
height: 300px;
transform-origin: center 275px;
transform: rotate(var(--minute));
}
.dial > .second {
top: 25px;
width: 2.5px;
height: 300px;
transform-origin: center 275px;
transform: rotate(var(--second));
}
对以上的 JavaScript 代码和 CSS 代码进行观察,相信聪明的您已经发现,我们在 JavaScript 中,通过 document.documentElement.style.setProperty 方法,给 CSS 声明了 --hour、--minute、--second 三个变量,其值为计算之后对应的角度值,还带有单位 deg。然后在 CSS 中对应的地方使用 var() 方法并传入对应的变量名,就可以取到相应的值。
这样,我们就获取了一个当前时间显示的效果。大功告成了……………………吗?
仔细观察就会发现,图中的当前时间是 10:46:11。在现实中,这个时候的时针和分针都不应该正正的指向对应值的刻度,而是会顺时针方向向前偏移对应的比例。
理想是美好的,现实问题也是赤裸裸的存在的:这样的理想情况,怎么实现呢?
首先,我们单独用一个变量 sStart 记录秒针的当前度数(也叫起始位置):
const sStart = 6 * second; // 设置秒指针的起始位置
分钟有 60 个刻度,那么 sStart / 60 就是分针应该在顺时针方向多偏移的角度。于是我们声明一个记录分针的当前度数的变量 mStart:
const mStart = 6 * minute + sStart / 60; // 设置分钟指针的起始位置
同样的道理,小时有 12 个刻度,那么 mStart / 12 就是时针应该在顺时针方向多偏移的角度。于是我们也声明一个记录时针的当前度数的变量 hStart:
const hStart = 30 * hour + mStart / 12; // 设置小时指针的起始位置
于是,整个设置实时时间的 setRealTime 函数就变成了这样:
/**
* @description 设置实时时间
*/
const setRealTime = () => {
const now = new Date(); // 获取当前时间
const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
const sStart = 6 * second; // 设置秒指针的起始位置
const mStart = 6 * minute + sStart / 60; // 设置分钟指针的起始位置
const hStart = 30 * hour + mStart / 12; // 设置小时指针的起始位置
document.documentElement.style.setProperty('--hour', `${hStart}deg`); // 根据当前小时设置小时指针的旋转角度
document.documentElement.style.setProperty('--minute', `${mStart}deg`); // 根据当前分钟设置分钟指针的旋转角度
document.documentElement.style.setProperty('--second', `${sStart}deg`); // 根据当前秒设置秒指针的旋转角度
}
得到的效果如下:
嗯~ o(* ̄▽ ̄*)o~~~~~~无论如何,看时针和分针,确实和现实中的钟表显示一致了。
三、让时钟动起来
前面“行为篇”中,我们是采用在设置实时时间的 setRealTime 函数中进行递归调用的。尽管我们只需要每隔一秒钟调用一次该函数,但是每次调用都会把上面繁琐的计算都运行一次,资源浪费不说,总是过不了心里的这一关。
设想一下,咱们已经根据当前时间让指针的起始位置都确定了。接下来只需要设置让时针自动每 12 个小时(216000 秒)绕一圈,让分针自动每一个小时(3600 秒)绕一圈,让秒针自动每 60 秒绕一圈,岂不美哉?
三个指针都是绕一圈后继续重复,需要具备两个条件:
-
需要知道每一个指针绕一圈之后的度数,即:起始位置 + 360 deg;
-
需要使用 CSS 动画来完成。
那么,我们先在设置实时时间的 setRealTime 函数中计算一下每一个指针绕一圈之后的度数,并继续通过 CSS4 变量传递给 CSS:
document.documentElement.style.setProperty('--toHour', `${hStart + 360}deg`); // 根据当前小时设置小时指针的旋转结束角度
document.documentElement.style.setProperty('--toMinute', `${mStart + 360}deg`); // 根据当前分钟设置分钟指针的旋转结束角度
document.documentElement.style.setProperty('--toSecond', `${sStart + 360}deg`); // 根据当前秒设置秒指针的旋转结束角度
这样,完整的设置实时时间的 setRealTime 函数就是这样的:
/**
* @description 设置实时时间
*/
const setRealTime = () => {
const now = new Date(); // 获取当前时间
const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
const sStart = 6 * second; // 设置秒指针的起始位置
const mStart = 6 * minute + sStart / 60; // 设置分钟指针的起始位置
const hStart = 30 * hour + mStart / 12; // 设置小时指针的起始位置
document.documentElement.style.setProperty('--hour', `${hStart}deg`); // 根据当前小时设置小时指针的旋转角度
document.documentElement.style.setProperty('--minute', `${mStart}deg`); // 根据当前分钟设置分钟指针的旋转角度
document.documentElement.style.setProperty('--second', `${sStart}deg`); // 根据当前秒设置秒指针的旋转角度
document.documentElement.style.setProperty('--toHour', `${hStart + 360}deg`); // 根据当前小时设置小时指针的旋转结束角度
document.documentElement.style.setProperty('--toMinute', `${mStart + 360}deg`); // 根据当前分钟设置分钟指针的旋转结束角度
document.documentElement.style.setProperty('--toSecond', `${sStart + 360}deg`); // 根据当前秒设置秒指针的旋转结束角度
}
传递的 --toHour、--toMinute、--toSecond 变量,需要在 CSS 中定义到动画的最后一帧中。于是我们在 CSS 中分别定义三个指针的旋转动画:
@keyframes hour {
to {
transform: rotate(var(--toHour));
}
}
@keyframes minute {
to {
transform: rotate(var(--toMinute));
}
}
@keyframes second {
to {
transform: rotate(var(--toSecond));
}
}
然后,在时针、分针、秒针元素对应的 CSS 中分别调用对应的动画:
.dial > .hour {
top: 125px;
width: 10px;
height: 200px;
transform-origin: center 175px;
transform: rotate(var(--hour)) ;
animation: hour 216000s linear infinite;
}
.dial > .minute {
top: 25px;
width: 7.5px;
height: 300px;
transform-origin: center 275px;
transform: rotate(var(--minute));
animation: minute 3600s linear infinite;
}
.dial > .second {
top: 25px;
width: 2.5px;
height: 300px;
transform-origin: center 275px;
transform: rotate(var(--second));
animation: second 60s linear infinite;
}
注意:三个动画在调用的时候都设置了匀速、重复;三个动画对应的时间都是秒数。
看一下效果:
这样,我们就用相对理想的方式把我们的时钟给完成了。
完整源代码
最后,还是把最终的完整源代码贡献出来,大家拿走不谢( ̄_, ̄ )
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clock</title>
<style type="text/css">
body {
background: #333;
}
.dial {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 600px;
height: 600px;
margin: auto;
border: 5px solid #fff;
border-radius: 50%;
}
.dial > span {
position: absolute;
top: 0;
right: 0;
left: 0;
display: block;
width: 5px;
height: 10px;
margin: auto;
background: #fff;
transform-origin: center 300px;
}
.dial > span.bold {
width: 7.5px;
height: 20px;
}
.dial > .pointer {
position: absolute;
right: 0;
left: 0;
margin: auto;
background: #fff;
border-radius: 5px;
}
.dial > .hour {
top: 125px;
width: 10px;
height: 200px;
transform-origin: center 175px;
transform: rotate(var(--hour)) ;
animation: hour 216000s linear infinite;
}
.dial > .minute {
top: 25px;
width: 7.5px;
height: 300px;
transform-origin: center 275px;
transform: rotate(var(--minute));
animation: minute 3600s linear infinite;
}
.dial > .second {
top: 25px;
width: 2.5px;
height: 300px;
transform-origin: center 275px;
transform: rotate(var(--second));
animation: second 60s linear infinite;
}
@keyframes hour {
to {
transform: rotate(var(--toHour));
}
}
@keyframes minute {
to {
transform: rotate(var(--toMinute));
}
}
@keyframes second {
to {
transform: rotate(var(--toSecond));
}
}
</style>
<script type="text/javascript">
/**
* @description 创建刻度
*/
const createScale = () => {
const oDiv = document.querySelector('.dial'); // 获取钟表盘面
for (let i = 0; i < 60; i++) {
const oSpan = document.createElement('span'); // 创建刻度元素
oSpan.style.transform = `rotate(${6 * i}deg)`; // 设置旋转刻度属性
if (i % 5 === 0) {
oSpan.className = 'bold'; // 每 5 个刻度,让其显示突出一些
}
oDiv.appendChild(oSpan); // 将刻度放到钟表盘面
}
setRealTime(); // 设置实时时间
}
/**
* @description 设置实时时间
*/
const setRealTime = () => {
const now = new Date(); // 获取当前时间
const [hour, minute, second] = [now.getHours(), now.getMinutes(), now.getSeconds()]; // 取出当前小时、分钟和秒
const sStart = 6 * second; // 设置秒指针的起始位置
const mStart = 6 * minute + sStart / 60; // 设置分钟指针的起始位置
const hStart = 30 * hour + mStart / 12; // 设置小时指针的起始位置
document.documentElement.style.setProperty('--hour', `${hStart}deg`); // 根据当前小时设置小时指针的旋转角度
document.documentElement.style.setProperty('--minute', `${mStart}deg`); // 根据当前分钟设置分钟指针的旋转角度
document.documentElement.style.setProperty('--second', `${sStart}deg`); // 根据当前秒设置秒指针的旋转角度
document.documentElement.style.setProperty('--toHour', `${hStart + 360}deg`); // 根据当前小时设置小时指针的旋转结束角度
document.documentElement.style.setProperty('--toMinute', `${mStart + 360}deg`); // 根据当前分钟设置分钟指针的旋转结束角度
document.documentElement.style.setProperty('--toSecond', `${sStart + 360}deg`); // 根据当前秒设置秒指针的旋转结束角度
}
window.onload = createScale;
</script>
</head>
<body>
<div class="dial">
<div class="pointer hour"></div>
<div class="pointer minute"></div>
<div class="pointer second"></div>
</div>
</body>
</html>
篇幅有限,很多地方都没有过多展开讲解了。之后,我们会写出更多有趣的文章,大家敬请期待!
关注我,为您奉上更多精彩内容!