基础问答
问题:你在写代码的过程中,在什么时候才会设置 setTimeout
的延时为 0?
回答:有如下几种情况
- 避免同步任务阻塞 UI,即在渲染较多数据的时候,可以通过 setTimeout 分批渲染。
const data = new Array(1000).fill(1).map((x, idx) => idx + 1);function render(list) {let index = 0;for (; index < list.length; index += 100) {console.log('current', index);const current = index;setTimeout(() => { console.log(list.slice(current, current + 100).join(','))}, 0);}
}render(data);
- 获取 DOM 元素的宽高,本质是根据事件循环机制调整了代码的执行顺序。
function App() {const dom = document.querySelector('#app');console.log(dom.height);setTimeout(() => dom.height, 0);
}
- 代码分片,古早技术,将同步代码分片执行,避免阻塞渲染。
扩展延伸
JavaScript 单线程:JavaScript 是单线程语言,这个是编程语言的设计,在同一时间只能执行一段代码,所有的任务都需要排队,而身为单线程,但是好像我们访问网页的时候还是那么快,这语言优势这么强?这是另一个问题,语言设计上是单线程,只能同步的执行代码,但是浏览器不是,他是多线程的,分出来一个 JS 主线程用于执行 JavaScript 代码,还有如 UI 线程,用于执行渲染等。在 JavaScript 中,通过事件循环来协调任务执行,实现异步编程。
事件循环:这个机制是 JavaScript 的一个核心机制,可以利用这个机制实现高并发,异步编程操作。
核心是 - 调用栈、任务队列、宏任务、微任务。
整个流程为 - JavaScript 代码按照代码依次执行时,检测到同步任务就进入调用栈执行,检测到宏任务,先压入宏任务队列,检测到微任务,则压入微任务队列,当本轮同步任务(宏任务)结束时,检测微任务队列,清空(即执行所有的微任务),这个检测的时机称为“微任务检查点”。
如图,伴随着每个宏任务执行,都有自己对应的微任务队列,直到微任务队列全部执行完成,才会开启下一个宏任务。
setTimeout(callback, delayTime)
API:在执行这个 API 时,JS 引擎会将 callback 函数封装成宏任务,挂载到延迟队列中,等待执行。这里再次引入了一个新的概念,延迟队列,这个是浏览器(或者引擎)实现的,当 JavaScript 创建定时器的时候,渲染进程就会将这个定时器的任务添加到延迟队列中。执行完一个任务,计算延迟队列中是否有到期的任务,有就执行,没有继续循环。
面试追问
- 延迟时间为 0,会立即执行吗?
不会,虽然我们设置为了 0,但是 setTimeout 的回调函数会被封装成一个宏任务,所以他需要等待同步任务执行结束后,从宏任务队列中取出来执行。此外,这个延迟时间虽然可以设置为 0,但是浏览器的最小执行时间实际是不一定的,Chrome 浏览器是 4ms。
- 那延迟时间设置为 400ms,会在 400ms 时执行吗?
不会,原因同上。setTimeout 只能做到“尽快执行”,而不是“立即执行”。
- 你在使用
setTimeout
的时候,有遇到过什么问题吗?
历史代码问题,存在比较多的 setTimeout
导致代码执行的结果不好理解。
this 指针问题,setTimeout
回调函数中的 this 和直觉不符,如果执行的回调函数是一个对象的方法,那么这个对象的方法中 this 并不是指向这个对象,而是全局。
长任务阻塞延迟的回调函数调用,如果当前任务执行的时间比较长,可能会导致回调函数等待。
浏览器优化问题,现在浏览器为了降低对电量的消耗,延长续航时间,会对后台界面的 setTimeout 执行时间间隔延长,一般会大于 1s,但是遇到过更久的,有一个多小时。
- 那有没有可以替代的 API?
有,和动画相关的可以使用 requestAnimationFrame
API 来替代,可以保持和浏览器渲染频率一致,而不需要计算每帧的间隔时间来延迟执行。
微任务可以使用 Promise 来创建。
- 实现一个简单的 setTimeout。
/*** 用 requestAnimationFrame 实现简易 setTimeout* @param {number} delay - 延迟时间(毫秒)* @returns {number} - RAF的ID,用于取消(对应clearTimeout)*/
function rafSetTimeout(callback, delay) {// 1. 记录延迟结束的目标时间(当前时间 + 延迟时间)const startTime = Date.now();const targetTime = startTime + delay;// 2. 定义递归执行的RAF回调函数function rafCallback() {// 3. 检查当前时间是否达到目标时间if (Date.now() >= targetTime) {// 达到目标时间,执行用户回调callback();} else {// 未达到,继续递归调用RAF,等待下一次重绘requestAnimationFrame(rafCallback);}}// 4. 启动第一次RAF,开始等待return requestAnimationFrame(rafCallback);
}/*** 对应 clearTimeout,取消 rafSetTimeout* @param {number} rafId - rafSetTimeout 返回的RAF ID*/
function rafClearTimeout(rafId) {cancelAnimationFrame(rafId);
}
- 经典题目,判断运行结果,这里给个简单的例子。
setTimeout(() => {console.log('回调1');
}, 0);// 插入同步任务
console.log('同步任务');setTimeout(() => {console.log('回调2');
}, 0);