3

由一个钟表引发的思考

 2 years ago
source link: https://innei.ren/posts/learning-process/thinking-with-js-event-loop
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
由一个钟表引发的思考 - 静かな森

最近突发奇想想做一个桌面时钟,利用 Übersicht 可以用前端的知识开发一个桌面小部件,如图:

UI 的部分我直接把几万年前做的时钟搬过来了,然后在业务逻辑上又做了一遍整体的梳理。之前的钟表是采用首次执行获取当前时间然后每秒再去计算在上一次摆动的角度上新增的角度,累加之后获得下一秒的摆动角度。主要代码:

js
// init
const time = new Date()
const minute = time.getMinutes()
const second = time.getSeconds()
const hour = time.getHours()

let minuteDeg =
    180 + 6 /* 360 / 60 */ * minute + 0.1 /* 360 / 3600 */ * second
let secondDeg = 180 + 6 /* 360 / 60 */ * second
let hourDeg =
    180 +
    30 /* 360 / 12 */ * hour +
    0.5 /* 360 / 720 */ * minute +
    0.00833333 /* 360 / 43200 */ * second

// 每秒动画
function setTime() {
  // 处理动画
  // springHand($minute, minuteDeg)
  // ...
  // 下一秒的角度
  secondDeg += 6 // 360 / 60
  minuteDeg += 0.1 // 360 / 3600
  hourDeg += 0.00833333 // 360 / 43200
}

setInterval(setTime, 1000)

之前在做这个钟的时候的确没有考虑周全,直接用了 setInterval 去不断的执行。而我们知道 JS 是个单线程的语言,同时只能做一件事,所以 1000ms 的延后其实并不是 1000ms,这个 1000ms 是空闲(Idle)之后的 1000ms,假设主进程将 timer 放到宏任务队列后,执行完微任务,可能会进行 UI rendering,然后开始执行计时器也就是宏任务。所以 1000ms 只是我们设定的,实际上还需要加上前面 UI rendering 和其他任务的执行开销操作。远不止 1000ms。上面代码改写成 setTimeout,然后通过 Performance API 测出两次执行之间的时间差。如下:

js
setTime()
let t = performance.now()
timer.current = setTimeout(function loop() {
  setTime()

  const now = performance.now()
  console.log(now - t)
  timer.current = setTimeout(loop, 1000)
  t = performance.now()
}, 1000)

每次居然都有 3-4ms 的性能开销,况且在计算成本上我已经很努力的去优化了,甚至我都没有每秒都去获取当前时间。我把 UI render 的部分注释再去测试计算开销,跑一万次取平均值。结果是只有 0.0016699999988079072ms,几乎可以忽略不计。所以问题就是 UI render 上,UI 操作是一个开销非常大的操作。

如果不去修正误差会怎么样,以我的方式,每秒去计算下一秒的摆动幅度,每次误差 3ms,1000 秒之后误差 3 秒,也就是 15 分钟大概会慢 3 秒。如果是每次都去实时获取当前时间呢?看起来UI 上的时间永远都是正确的,但是如果盯着看的话,无知无觉中会少了 3 秒。

那么去调整误差也是比较简单的,可以通过 setTimeout 去修正误差。如:

js
let t = performance.now()
let count = 0
timer.current = setTimeout(function loop() {
      setTime()
      const now = performance.now()
      const offset = (now - t) / count / 1000
      timer.current = setTimeout(loop, 1000 - offset)
      count++
}, 1000)

通过上面的方式的微调之后,再来看看误差值。

对比上面的 3ms 误差少了平均 2ms,所以在一段时间之后还是要重置一遍,重新获取一下当前时间。

如果主线程只做一件事情就是计时,那 1000ms 到底准不准呢。

js
t = performance.now()
setInterval(() => {
  let now = performance.now()
  console.log(now - t)
  t = now
}, 1000)

测试与 Chrome 96

居然也是不准的,MDN 给出计时器最小误差在 4ms,而且每个浏览器还不一样。所以为什么一般不用了 setInterval 而是用 setTimeout 去调整误差了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK