前言
通常写倒计时效果,用的是 setInterval,但这会引发一些问题,最常见的问题就是定时器不准。
如果只是普通的动画效果,倒也无所谓,但倒计时这种需要精确到毫秒级别的,就不行了,否则活动都结束了,用户的界面上倒计时还在走,但是又参加不了活动,会被投诉的╮(╯▽╰)╭
一、 知识铺垫
1. setInterval 定时器
先说本文的主角 setInterval,MDN web doc 对其的解释是:
setInterval() 方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟。
返回一个 intervalID。(可用于清除定时器)
语法: let intervalID = window.setInterval(func, delay[, param1, param2, ...]);
例:
值得注意的是,在 setInterval 里面使用 this 的话,this 指向的是 window 对象,可以通过 call、apply 等方法改变 this 指向。
setTimeout 与 setInterval 类似,只不过延迟 n 毫秒执行函数一次,且不需要手动清除。
至于 setTimeout 和 setInterval 的运行原理,就要牵扯到另一个概念: event loop (事件循环)。
2. 浏览器的 Event Loop
JavaScript 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中,若遇到异步的代码,会被挂起并加入到 task (有多种 task) 队列中。
一旦执行栈为空, event loop 就会从 task 队列中拿出需要执行的代码并放入执行栈中执行。
有了 event loop,使得 JavaScript 具备了异步编程的能力。(但本质上,还是同步行为)
先看一道经典的面试题:
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
console.log('Promise');
resolve()
}).then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
console.log('Scritp end');
打印顺序为:
- “Script start”
- “Promise”
- “Script end”
- “Promise 1”
- “Promise 2”
- “setTimeout”
至于为什么 setTimeout 设置为 0,却在最后被打印,这就涉及到 event loop 中的微任务和宏任务了。
2.1 宏任务和微任务
不同的任务源会被分配到不同的 task 队列中,任务源可分为微任务( microtask )和宏任务( macrotask ).
在 ES6 中:
- microtask 称为 Job
- macrotask 称为 Task
macro-task(Task): 一个 event loop 有一个或者多个 task 队列。task 任务源非常宽泛,比如 ajax 的 onload,click 事件,基本上我们经常绑定的各种事件都是 task 任务源,还有数据库操作(IndexedDB ),需要注意的 是setTimeout、setInterval、setImmediate