今天继续 JavaScript 核心复习,重点放在事件循环、宏任务/微任务以及 Promise/async/await 上,最后还整理了防抖节流的手写实现。这些概念是面试中的高频考点,也是理解异步编程和性能优化的关键。

以下是当天的问答整理。


Q1:事件循环(Event Loop)是什么?请简述执行顺序。

我的回答:

  • 先执行所有同步代码 → 执行所有微任务(比如接口调用、数据请求) → 渲染更新页面 → 执行下一个宏任务(比如 DOM 点击事件、setTimeout)。
  • JS 代码是单线程,异步操作是由浏览器(或 Node.js)在幕后进行的,当同步代码执行完后,再把异步操作的结果回调到主线程执行。

补充:

  • “接口调用、数据请求”本身不是微任务(同步代码),它们的回调(如 fetch().then() 里的 .then)才是微任务。setTimeout 的回调是宏任务。
  • 微任务可以“插队”。

Q2:常见的宏任务和微任务有哪些?如果任务中产生新的微任务,会怎么处理?

我的回答:

  • 常见的宏任务:DOM 点击事件、setTimeout()。
  • 常见的微任务:promise.then()、async/await 等。
  • 如果宏任务中产生新的微任务,那么先会把微任务清空再执行下一个宏任务。

补充:

  • 宏任务(MacroTask) 还包括:setInterval、I/O(文件读写、网络请求完成后的回调)、requestAnimationFrame(严格说它属于渲染前执行,但常被归为宏任务)、postMessage、MessageChannel 等。
  • 微任务(MicroTask) 还包括:queueMicrotask、MutationObserver、process.nextTick(Node.js 环境)。
  • 在当前宏任务执行过程中,如果产生了新的微任务(比如在 Promise.then 中再注册一个 then),这些新微任务会追加到微任务队列末尾,并在当前宏任务结束前、下一个宏任务开始前被清空。也就是说,微任务可以连续嵌套,但必须清空整个队列才会执行下一个宏任务。

Q3:Promise 的基本用法是什么?then、catch、finally 的作用?

我的回答:

  • 具体概念相对理解较差,then 可以请求结果,catch 是错误处理。

补充:

  • Promise 是一个对象,代表一个异步操作的最终完成(或失败)及其结果值。
  • 三种状态:pending(进行中)fulfilled(已成功)rejected(已失败)
  • then 接收两个可选参数:onFulfilled 和 onRejected,分别处理成功和失败。它返回一个新的 Promise,支持链式调用。
  • catch 是 .then(null, onRejected) 的语法糖,专门处理错误。
  • finally 无论成功或失败都会执行,通常用于清理操作(如关闭加载动画),不接收参数。
    const p = new Promise((resolve, reject) => {
      // 异步操作
      if (成功) resolve(value);
      else reject(error);
    });
    
    p.then(value => { /* 处理成功 */ })
     .catch(error => { /* 处理失败 */ })
     .finally(() => { /* 清理 */ });

Q4:async/await 的原理和使用?请结合代码解释。

我的回答:

  • async 返回一个 Promise 对象。
  • await 代码旨在 Promise 成功之后执行。
    async function fetchData() {
      try {
        const res = await fetch('/api/data');
        const data = res.data;
      } catch (err) {
        console.error(err);
      }
    }

补充:

  • async 函数总是返回一个 Promise。如果返回值不是 Promise,会被 Promise.resolve 包装成Promise状态。
  • await 只能用在 async 函数内部(顶级 await 已在部分环境支持)。它会暂停当前 async 函数的执行,等待 Promise 完成,然后继续执行后面的代码。这个“暂停”是非阻塞的,不会阻塞事件循环。
    // 测试:await 不阻塞定时器(宏任务)
    async function test() {
      console.log('test 开始');
      await new Promise(resolve => setTimeout(resolve, 1000));
      console.log('test 结束');
    }
    
    test();
    // 定时器会正常执行,说明事件循环没被阻塞
    setTimeout(() => {
      console.log('定时器执行');
    }, 500);
    
    // 输出顺序:
    // test 开始
    // 定时器执行(500ms 后,test 还在暂停)
    // test 结束(1000ms 后)
  • await 后面通常跟一个 Promise,如果不是 Promise,则会被转换为立即 resolved 的 Promise。
  • 错误处理:推荐用 try/catch 捕获 await 的 reject,也可以在 async 函数后面链式调用 .catch()。
  • async/await 是 Promise 的语法糖,使异步代码看起来像同步,更易读。

Q5:看代码写输出(事件循环综合题)

题目:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
}, 0);

Promise.resolve().then(() => {
  console.log(4);
  setTimeout(() => {
    console.log(5);
  }, 0);
});

console.log(6);

我的回答:顺序是 1, 6, 4, 2, 3, 5。

解析:

  1. 同步代码:输出 1,然后 setTimeout 回调(宏任务)被放入宏任务队列,Promise.then(微任务)被放入微任务队列,最后输出 6。此时输出顺序:1, 6

  2. 当前宏任务结束,清空微任务队列:微任务中有第一个 Promise.then(输出 4),执行它,输出 4,并且它内部又有一个 setTimeout(宏任务)被加入宏任务队列。此时微任务队列清空,输出 4。

  3. 取出第一个宏任务:第一个 setTimeout 回调执行,输出 2,内部有 Promise.resolve().then(微任务),加入微任务队列。此时输出 2

  4. 再次清空微任务:微任务队列中的 then 执行,输出 3。

  5. 取出第二个宏任务:第二个 setTimeout 回调执行,输出 5。


手写题1:实现 Promise.all

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }
    const results = [];
    let completed = 0;
    const total = promises.length;

    if (total === 0) {
      return resolve(results);
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        value => {
          results[index] = value;
          completed++;
          if (completed === total) {
            resolve(results);
          }
        },
        reason => {
          reject(reason);
        }
      );
    });
  });
}

// 测试
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const p3 = 3;

myPromiseAll([p1, p2, p3]).then(console.log); // 约100ms后输出 [1, 2, 3]

要点:

  • 返回一个新的 Promise。
  • 用 Promise.resolve 统一处理非 Promise 项。
  • 保持结果顺序与输入一致(通过 results[index] 赋值)。
  • 任何一个失败立即 reject(Promise 的错误会向上冒泡)。

手写题 2:防抖与节流

防抖和节流是前端性能优化的常用手段,面试中也常考手写。

防抖(Debounce)

作用:在事件被触发 delay 毫秒后执行回调,如果在这段时间内再次触发,则重新计时。适用于输入框搜索、窗口 resize ,支付等场景。

我的代码:

function debounce(func, delay) {
  let time;
  return function (...arg) {
    clearTimeout(time);
    time = setTimeout(() => {
      //等价于 handleClick.apply(this, [e])
      func.apply(this, arg);
    }, delay);
  };
}

解释:

  • 返回一个闭包函数,防止每次还没操作就调用,每次事件触发时清除之前的定时器,重新设置。
  • 使用 apply 确保原函数的 this 和参数正确传递。
  • 最后一次触发后的 delay 毫秒内没有新触发,则执行原函数。

节流(Throttle)

作用:保证在 delay 毫秒内最多执行一次回调。适用于滚动、鼠标移动等高频事件。

我的代码:

function throttle(func, delay) {
  let beginTime = 0;
  return function (...arg) {
    let cur = new Date().getTime();
    if (cur - beginTime > delay) {
      func.apply(this, arg);
      beginTime = cur;
    }
  };
}

解释:

  • 记录上次执行时间 beginTime。
  • 每次触发判断当前时间与上次执行时间差是否大于 delay,是则执行并更新 beginTime。
  • 第一次触发会立即执行(因为 beginTime 为 0)。

使用示例

const button = document.getElementById('input');
const play = () => console.log('已点击');

// 防抖:连续点击只会在最后一次点击后 1 秒执行
button.addEventListener('click', debounce(play, 1000));

// 节流:每秒最多执行一次
button.addEventListener('click', throttle(play, 1000));

补充:节流还有定时器版,可以保证最后一次触发也执行(但可能会延迟),时间戳版适合需要立即响应的场景。


今日知识点总结

概念 要点
事件循环 同步代码 → 清空微任务 → 取一个宏任务执行 → 循环
宏任务 setTimeout、setInterval、I/O、UI 事件、requestAnimationFrame
微任务 Promise.then/catch/finally、queueMicrotask、MutationObserver
Promise 三种状态(pending/fulfilled/rejected),链式调用,错误传递
async/await async 函数返回 Promise,await 暂停执行直到 Promise 完成
Promise.all 所有成功则返回结果数组,任一失败则立即 reject
防抖 延迟执行,期间重新计时
节流 固定频率执行,保证间隔
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐