兄弟们,手写 Promise 这道题,我二面栽了

0 阅读5分钟

先说个丢人的。上个月面某大厂,一面八股背得贼溜,事件循环、闭包、原型链一套一套的,面试官连连点头。我心里正飘呢,二面轻描淡写一句:「来,手写个 Promise,then catch finally 都实现,循环引用处理一下」。

我当场裂开。

平时 await 一把梭,真让我从零写一个,手比脑子快——resolvePromise 里 thenable 和循环引用那块直接卡死,憋了十分钟写了个四不像,连微任务都忘包了。出来我就知道,这 offer 悬了。

不是委屈,是后怕。我写前端三四年,Promise 天天用,真让我造一遍轮子居然造不利索。这哪是面试翻车,是我一直赖在「会用就行」的舒适区,从没把轮子拆开看过。

说真的,手写题这东西特别能验人。你框架用得再溜,手写一道响应式源码,是骡子是马立马现形。面试官也精,与其听你吹项目多牛,不如扔一道题看你能不能把原理落到代码上。所以这几年前端面试,手写题和源码解析一直是硬通货——你刷十篇业务总结,不如真刀真枪手写五道题来得实在。

出来之后越想越气,跟自己较上了劲:与其每次面试前临时抱佛脚背八股,不如把高频手写题自己写一遍、跑一遍、讲明白一遍。一个周末肝完,攒了 30 道,顺手把 Vue3 / React18 原理脑图也画了。今天直接白送,别灌水,纯干货。

我特意没整「100 题」那种凑数合集。30 道,每道带代码 + 解析 + 考点三件套,复制进 node 就能跑。比看别人贴的结论强十倍——你跑一遍才会发现「卧槽我这里少判了个状态」。

上真家伙

Promise 核心实现(这题不过,别的都白搭)

class MyPromise {
  static PENDING = 'pending';
  static FULFILLED = 'fulfilled';
  static REJECTED = 'rejected';
  constructor(executor) {
    this.state = MyPromise.PENDING;
    this.value = undefined; this.reason = undefined;
    this.onFulfilledCallbacks = []; this.onRejectedCallbacks = [];
    const resolve = (value) => {
      if (this.state === MyPromise.PENDING) {
        this.state = MyPromise.FULFILLED; this.value = value;
        this.onFulfilledCallbacks.forEach((fn) => fn());
      }
    };
    const reject = (reason) => {
      if (this.state === MyPromise.PENDING) {
        this.state = MyPromise.REJECTED; this.reason = reason;
        this.onRejectedCallbacks.forEach((fn) => fn());
      }
    };
    try { executor(resolve, reject); } catch (e) { reject(e); }
  }
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
    onRejected = typeof onRejected === 'function' ? onRejected : (r) => { throw r; };
    const promise2 = new MyPromise((resolve, reject) => {
      const handle = (fn, val) => queueMicrotask(() => {
        try { const x = fn(val); resolvePromise(promise2, x, resolve, reject); }
        catch (e) { reject(e); }
      });
      if (this.state === MyPromise.FULFILLED) handle(onFulfilled, this.value);
      else if (this.state === MyPromise.REJECTED) handle(onRejected, this.reason);
      else {
        this.onFulfilledCallbacks.push(() => handle(onFulfilled, this.value));
        this.onRejectedCallbacks.push(() => handle(onRejected, this.reason));
      }
    });
    return promise2;
  }
  catch(onRejected) { return this.then(null, onRejected); }
}
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) return reject(new TypeError('Chaining cycle'));
  if (x && (typeof x === 'object' || typeof x === 'function')) {
    let called = false;
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(x, (y) => {
          if (called) return; called = true; resolvePromise(promise2, y, resolve, reject);
        }, (e) => { if (called) return; called = true; reject(e); });
      } else resolve(x);
    } catch (e) { if (called) return; called = true; reject(e); }
  } else resolve(x);
}

坑全在 resolvePromiseif (promise2 === x) 拦循环引用;返回的对象带 then 就递归 resolve(thenable 穿透)。这两点答出来,面试官基本就点头了。还有 then 回调一定用微任务包,不然时序对不上——我当时就是栽这儿。

防抖节流(90% 人只写得出半截)

function debounce(fn, wait = 300, immediate = false) {
  let timer = null, invoked = false;
  const debounced = function (...args) {
    if (timer) clearTimeout(timer);
    if (immediate && !invoked) { fn.apply(this, args); invoked = true; }
    timer = setTimeout(() => {
      if (!immediate) fn.apply(this, args);
      invoked = false; timer = null;
    }, wait);
  };
  debounced.cancel = () => { clearTimeout(timer); timer = null; invoked = false; };
  return debounced;
}
function throttle(fn, wait = 300) {
  let last = 0, timer = null;
  return function (...args) {
    const now = Date.now();
    const remaining = wait - (now - last);
    if (remaining <= 0) {
      if (timer) { clearTimeout(timer); timer = null; }
      last = now; fn.apply(this, args);
    } else if (!timer) {
      timer = setTimeout(() => { last = Date.now(); timer = null; fn.apply(this, args); }, remaining);
    }
  };
}

别只会 setTimeout 版。immediate 首触发、cancel 取消、节流里时间戳版 vs 定时器版——时间戳版首调立即执行但末尾漏一次,定时器版末尾补一刀,remaining 算窗口剩余是精髓。我第一次写节流就只写了时间戳版,被追问「末尾那次不执行合理吗」直接卡壳。

Vue3 响应式(框架原理真·重头戏,二三线厂也爱问)

let activeEffect = null;
const targetMap = new WeakMap();
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));
  dep.add(activeEffect);
}
function trigger(target, key) {
  const dep = targetMap.get(target) && targetMap.get(target).get(key);
  dep && [...dep].forEach((effect) => effect());
}
function reactive(target) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key);
      const res = Reflect.get(obj, key, receiver);
      return typeof res === 'object' && res !== null ? reactive(res) : res;
    },
    set(obj, key, value, receiver) {
      const ok = Reflect.set(obj, key, value, receiver);
      trigger(obj, key); return ok;
    }
  });
}

Proxy 拦 get/set,get 时 track 收集依赖、set 时 trigger 触发更新,WeakMaptarget -> key -> Set 三层存。自己敲一遍,你就懂 Vue3 凭啥比 Object.defineProperty 香——数组、新增属性不用挨个劫持,WeakMap 还不挡原对象回收。光会背「Vue3 用 Proxy」没用,让你手写 track/trigger 哑火照样挂。

我踩过的坑,兄弟们别重蹈

  • 只背不写:看十遍 Promise 实现,不如关掉网页自己写一遍。写不出就是没真懂。
  • 忽略边缘:面试官多半不考主流程,考循环引用、thenable、空参、new 调 bind 这些犄角旮旯。
  • 框架只背概念:「响应式是 Proxy」谁都会,手写 track/trigger 就露馅。
  • 防抖节流混为一谈:一个管「最后执行」,一个管「频率上限」,讲不清区别等于白答。

包里都有啥

30 题六类:异步与 Promise、this 与原型、函数进阶、数组与对象、框架原理、工程与算法。从 call/apply/bindcurrycompose、深拷贝、大数相加、千分位、懒加载,面试常客基本齐了。

外加两张脑图当骨架:

  • Vue3 原理:响应式 / 运行时 / 编译 / 性能
  • React18 原理:Fiber / 调和 / Hooks / 并发 / 事件

目录:

前端面试手写题宝典/
├─ 前端面试手写题宝典.pdf        # 30 题,代码+解析+考点
├─ Vue3底层原理思维导图.svg
├─ React18底层原理思维导图.svg
└─ README.md

怎么用

PDF 当刷题册,先自己写再对答案;两张 SVG 脑图像地图,面试前扫一遍心里有底。包里带了生成脚本,想加题自己改数据重跑就完事。

怎么拿

代码都是面试向精简实现,讲思路为主,生产请以官方源码为准。欢迎转发,但别拿去包装倒卖,那样真没劲。也祝在看的同学,下次面试手写题下笔如有神,别像我一样当场裂开。见评论区