【每日一面】setTimeout 延时为 0 的情况

基础问答

问题 :你在写代码的过程中,在什么时候才会设置 setTimeout 的延时为 0?

回答:有如下几种情况

  1. 避免同步任务阻塞 UI,即在渲染较多数据的时候,可以通过 setTimeout 分批渲染。
javascript 复制代码
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);
  1. 获取 DOM 元素的宽高,本质是根据事件循环机制调整了代码的执行顺序。
typescript 复制代码
function App() {
  const dom = document.querySelector('#app');
  console.log(dom.height);
  setTimeout(() => dom.height, 0);
}
  1. 代码分片,古早技术,将同步代码分片执行,避免阻塞渲染。

扩展延伸

JavaScript 单线程 :JavaScript 是单线程语言,这个是编程语言的设计,在同一时间只能执行一段代码,所有的任务都需要排队,而身为单线程,但是好像我们访问网页的时候还是那么快,这语言优势这么强?这是另一个问题,语言设计上是单线程,只能同步的执行代码,但是浏览器不是,他是多线程的,分出来一个 JS 主线程用于执行 JavaScript 代码,还有如 UI 线程,用于执行渲染等。在 JavaScript 中,通过事件循环来协调任务执行,实现异步编程。

事件循环:这个机制是 JavaScript 的一个核心机制,可以利用这个机制实现高并发,异步编程操作。

核心是 - 调用栈、任务队列、宏任务、微任务

整个流程为 - JavaScript 代码按照代码依次执行时,检测到同步任务就进入调用栈执行,检测到宏任务,先压入宏任务队列,检测到微任务,则压入微任务队列,当本轮同步任务(宏任务)结束时,检测微任务队列,清空(即执行所有的微任务),这个检测的时机称为"微任务检查点"。

如图,伴随着每个宏任务执行,都有自己对应的微任务队列,直到微任务队列全部执行完成,才会开启下一个宏任务。

setTimeout(callback, delayTime) API:在执行这个 API 时,JS 引擎会将 callback 函数封装成宏任务,挂载到延迟队列中,等待执行。这里再次引入了一个新的概念,延迟队列,这个是浏览器(或者引擎)实现的,当 JavaScript 创建定时器的时候,渲染进程就会将这个定时器的任务添加到延迟队列中。执行完一个任务,计算延迟队列中是否有到期的任务,有就执行,没有继续循环。

面试追问

  1. 延迟时间为 0,会立即执行吗?

不会,虽然我们设置为了 0,但是 setTimeout 的回调函数会被封装成一个宏任务,所以他需要等待同步任务执行结束后,从宏任务队列中取出来执行。此外,这个延迟时间虽然可以设置为 0,但是浏览器的最小执行时间实际是不一定的,Chrome 浏览器是 4ms。

  1. 那延迟时间设置为 400ms,会在 400ms 时执行吗?

不会,原因同上。setTimeout 只能做到"尽快执行",而不是"立即执行"。

  1. 你在使用 setTimeout 的时候,有遇到过什么问题吗?

历史代码问题,存在比较多的 setTimeout 导致代码执行的结果不好理解。

this 指针问题,setTimeout 回调函数中的 this 和直觉不符,如果执行的回调函数是一个对象的方法,那么这个对象的方法中 this 并不是指向这个对象,而是全局。

长任务阻塞延迟的回调函数调用,如果当前任务执行的时间比较长,可能会导致回调函数等待。

浏览器优化问题,现在浏览器为了降低对电量的消耗,延长续航时间,会对后台界面的 setTimeout 执行时间间隔延长,一般会大于 1s,但是遇到过更久的,有一个多小时。

  1. 那有没有可以替代的 API?

有,和动画相关的可以使用 requestAnimationFrame API 来替代,可以保持和浏览器渲染频率一致,而不需要计算每帧的间隔时间来延迟执行。

微任务可以使用 Promise 来创建。

  1. 实现一个简单的 setTimeout。
javascript 复制代码
/**
 * 用 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);
}
  1. 经典题目,判断运行结果,这里给个简单的例子。
javascript 复制代码
setTimeout(() => {
  console.log('回调1');
}, 0);

// 插入同步任务
console.log('同步任务');

setTimeout(() => {
  console.log('回调2');
}, 0);