系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)



前言

上一篇整理了深拷贝、防抖节流与发布订阅;本篇延续「能写能讲」路线,聚焦 Promise 手写async/await 与 Generator 的关系、以及性能向的 LRU 缓存虚拟列表。其中 Promise 要能说清三态、链式 then、错误穿透与 Promise.all 的失败语义;LRU 与虚拟列表则常出现在大厂手写或项目优化题中。以下按「Promise → async/Generator → LRU → 虚拟列表」展开,并与第 08 篇宏任务/微任务衔接阅读。


一、Promise 基础与三态

1.1 定义

  1. Promise 表示异步操作的最终结果,状态仅有三种:pending(待定)fulfilled(已成功)rejected(已失败)
  2. 状态只能从 pendingfulfilled / rejected,一旦定型(settled)不可再变
  3. .then(onFulfilled, onRejected) 返回新的 Promise,从而支持链式调用。
  4. 值穿透:若 onFulfilled 不是函数,前一个 Promise 的 resolved 值会原样传递到下一个 thenonRejected 非函数时 rejected 值同理穿透)。

1.2 与事件循环的关系(复习)

  • new Promise(executor) 里的 executor 同步执行
  • .then / .catch / .finally 的回调进入微任务队列(见第 08 篇)。
  • 面试常考:「Promise 本身是同步的」指 executor 同步;异步感来自 then 回调延后到微任务
console.log("1");
new Promise((resolve) => {
  console.log("2");
  resolve();
}).then(() => console.log("3"));
console.log("4");
// 1 → 2 → 4 → 3

二、手写 Promise(核心版)

面试不要求与规范逐行一致,但需覆盖:then 链式值穿透返回 thenable 时等待错误捕获Promise.all

2.1 状态与构造函数

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  #state = PENDING;
  #value = undefined;
  #callbacks = [];

  constructor(executor) {
    const resolve = (value) => {
      if (this.#state !== PENDING) return;
      this.#state = FULFILLED;
      this.#value = value;
      this.#runCallbacks();
    };
    const reject = (reason) => {
      if (this.#state !== PENDING) return;
      this.#state = REJECTED;
      this.#value = reason;
      this.#runCallbacks();
    };
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  #runCallbacks() {
    queueMicrotask(() => {
      this.#callbacks.forEach(({ onFulfilled, onRejected, resolve, reject }) => {
        if (this.#state === FULFILLED) {
          this.#handle(onFulfilled, resolve, reject);
        } else {
          this.#handle(onRejected, resolve, reject);
        }
      });
      this.#callbacks = [];
    });
  }

  #handle(fn, resolve, reject) {
    try {
      const x =
        typeof fn === "function" ? fn(this.#value) : this.#value;
      this.#resolvePromise(x, resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  #resolvePromise(x, resolve, reject) {
    if (x instanceof MyPromise) {
      x.then(resolve, reject);
      return;
    }
    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;
              this.#resolvePromise(y, resolve, reject);
            },
            (r) => {
              if (called) return;
              called = true;
              reject(r);
            }
          );
          return;
        }
      } catch (e) {
        if (called) return;
        reject(e);
        return;
      }
    }
    resolve(x);
  }

  then(onFulfilled, onRejected) {
    const realOnFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (v) => v;
    const realOnRejected =
      typeof onRejected === "function" ? onRejected : (e) => {
          throw e;
        };

    return new MyPromise((resolve, reject) => {
      const run = () => {
        this.#handle(
          this.#state === FULFILLED ? realOnFulfilled : realOnRejected,
          resolve,
          reject
        );
      };
      if (this.#state === PENDING) {
        this.#callbacks.push({
          onFulfilled: realOnFulfilled,
          onRejected: realOnRejected,
          resolve,
          reject,
        });
      } else {
        run();
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(onFinally) {
    return this.then(
      (v) => MyPromise.resolve(onFinally?.()).then(() => v),
      (e) =>
        MyPromise.resolve(onFinally?.()).then(() => {
          throw e;
        })
    );
  }

  static resolve(val) {
    if (val instanceof MyPromise) return val;
    return new MyPromise((r) => r(val));
  }

  static reject(reason) {
    return new MyPromise((_, rej) => rej(reason));
  }
}

要点

  • #resolvePromise:处理 then 回调返回值;若为 Promise 或 thenable,需等其状态再 resolve/reject 外层(模拟规范中的 Promise Resolution Procedure 简化版)。
  • 非函数回调then 里用恒等 / 抛错实现值穿透rejected 穿透
  • 微任务用 queueMicrotask;不支持的环境可换 setTimeout(fn, 0) 近似(顺序与真实微任务略有差异,面试可说明)。

2.2 链式调用示例

MyPromise.resolve(1)
  .then((x) => x + 1)
  .then((x) => x * 3)
  .then(console.log); // 6

MyPromise.resolve(1)
  .then(() => {
    throw new Error("fail");
  })
  .catch((e) => console.log(e.message)); // fail

2.3 手写 Promise.all

MyPromise.all = function (iterable) {
  return new MyPromise((resolve, reject) => {
    const arr = [...iterable];
    if (arr.length === 0) return resolve([]);
    const result = [];
    let count = 0;
    arr.forEach((p, i) => {
      MyPromise.resolve(p).then(
        (val) => {
          result[i] = val;
          if (++count === arr.length) resolve(result);
        },
        reject
      );
    });
  });
};

面试聚焦

  1. 一个 rejected 即整体 reject(快速失败),已成功的结果不会取消,但调用方通常已进入 catch。
  2. 传入非 Promise 会经 Promise.resolve 包装。
  3. 空数组 Promise.all([]) 立即 resolve([])
  4. Promise.allSettled 对比:后者等全部结束,返回 { status, value/reason }[],不因单个失败而中断。
// 原生对比
Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error("x")),
]).catch((e) => console.log(e.message)); // 'x' — 整体失败

Promise.allSettled([
  Promise.resolve(1),
  Promise.reject(new Error("x")),
]).then(console.log);
// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: Error: x }
// ]

2.4 错误穿透

  • catch 本质是 then(null, onRejected)
  • 链中 throw 或返回 rejected Promise 会使后续 fulfilled 回调跳过,直到遇到 rejected 处理
  • catch 放在链中间只能捕获它之前未处理的 rejection;之后的 then 会拿到 catch 的返回值(或继续抛错)。
Promise.resolve()
  .then(() => {
    throw new Error("a");
  })
  .then(() => console.log("不会执行"))
  .catch((e) => console.log(e.message)) // a
  .then(() => console.log("catch 之后仍可 then")); // 会执行

三、async/await 与 Generator

3.1 async/await 是什么

  1. async 函数一定返回 Promise;函数内 return v 等价于 Promise.resolve(v)throw e 等价于 Promise.reject(e)
  2. await 暂停当前 async 函数执行,等待 Promise 定型;resolved 得到值,rejected 相当于 throw
  3. await 不阻塞主线程:等待期间事件循环仍可处理其他任务;async 函数在 await 之后的代码相当于 then 回调(微任务)。
const delay = (ms, val) =>
  new Promise((r) => setTimeout(() => r(val), ms));

const fetchUser = async (id) => {
  const profile = await delay(100, { id, name: "Tom" });
  const posts = await delay(200, ["post1", "post2"]);
  return { ...profile, posts };
};

fetchUser(1).then(console.log);

3.2 易混点

写法 行为
for 里连续 await 串行,总耗时约为各次之和
await Promise.all([...]) 并行,总耗时约为最慢的一次
async 里未 await 的 Promise 不会自动等待,可能产生未捕获 rejection
// 串行
for (const id of [1, 2, 3]) await delay(100, id);

// 并行
await Promise.all([1, 2, 3].map((id) => delay(100, id)));

3.3 Generator 与自动执行器(理解 async 本质)

Generatorfunction*)通过 yield 暂停、next() 恢复,返回 { value, done }。早期用 co 类库把 Generator 当成「同步写法写异步」的载体;async/await 可看作该模式的内置语法糖

function* gen() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
}

function run(generator) {
  const g = generator();
  function step(val) {
    const result = g.next(val);
    if (result.done) return Promise.resolve(result.value);
    return Promise.resolve(result.value).then(
      (data) => step(data),
      (err) => g.throw(err)
    );
  }
  return step();
}

run(gen).then(console.log); // 3(需调整 gen 内 yield 赋值逻辑时按题意修改)

更直观的「yield Promise」示例:

function* fetchData() {
  const user = yield delay(100, { id: 1 });
  const list = yield delay(100, ["a", "b"]);
  return { user, list };
}

run(fetchData).then(console.log);
// { user: { id: 1 }, list: ['a', 'b'] }

面试一句话async/await = Generator + 自动执行器 + Promise;不必手写 co,但要能解释「await 背后是把后续代码放进 then」。


四、LRU 缓存(Least Recently Used)

4.1 含义

LRU:容量满时淘汰最久未使用的项。要求 get / put 均为 O(1)(面试常用 哈希表 + 双向链表;工程与手写简答可用 Map 利用「插入顺序」)。

4.2 基于 Map 的实现

ES2015 的 Map 按插入顺序迭代;get 时先删再 set 可把项移到「最新」;满容时删 第一个 key(最久未用)。

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) return -1;
    const val = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, val);
    return val;
  }

  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      const oldest = this.cache.keys().next().value;
      this.cache.delete(oldest);
    }
    this.cache.set(key, value);
  }
}

const lru = new LRUCache(2);
lru.put(1, 1);
lru.put(2, 2);
lru.get(1); // 1
lru.put(3, 3); // 淘汰 key 2
lru.get(2); // -1

复杂度getput 平均 O(1);空间 O(capacity)

口述:Map 维护 key → value;每次访问把 key 挪到「尾部」;超容删「头部」最旧 key。


五、虚拟列表(Virtual List)

5.1 为什么需要

长列表若全量渲染 DOM(如 1 万行表格),会导致节点过多、滚动卡顿。思路:只渲染可视区域 + 缓冲的项,用占位高度撑开滚动条,滚动时复用节点并更新偏移。

5.2 核心公式

设每项固定高度 itemH,容器可视高度 viewH,滚动距离 scrollTop

  • 起始下标start = Math.floor(scrollTop / itemH)
  • 结束下标end = start + Math.ceil(viewH / itemH) + buffer(buffer 常取 1~2 行,减少白屏)
  • 总高度totalH = items.length * itemH
  • 每项定位top = i * itemH(绝对定位或 transform: translateY

5.3 简易实现(固定行高)

const createVirtualList = (container, items, itemH = 40, renderFn) => {
  const viewH = container.clientHeight;
  const totalH = items.length * itemH;
  const padEl = document.createElement("div");
  padEl.style.height = `${totalH}px`;
  padEl.style.position = "relative";
  container.appendChild(padEl);

  const render = () => {
    const start = Math.floor(container.scrollTop / itemH);
    const end = Math.min(
      start + Math.ceil(viewH / itemH) + 1,
      items.length
    );
    padEl.innerHTML = "";
    for (let i = start; i < end; i++) {
      const el = renderFn(items[i], i);
      el.style.position = "absolute";
      el.style.top = `${i * itemH}px`;
      el.style.height = `${itemH}px`;
      padEl.appendChild(el);
    }
  };

  container.addEventListener("scroll", render);
  render();
};

工程增强(口述即可):动态行高需维护前缀和数组求每项 top;React 可用 react-window / react-virtualized;滚动用 transform 有时更利于合成层。

复杂度:渲染节点数 O(可视行数),与总条数 N 解耦;滚动时重算 O(1)(固定行高)。


六、速查与面试对照

主题 必记
Promise 三态 pending → fulfilled / rejected,不可逆
executor / then executor 同步;then 回调微任务
链式 then 返回新 Promise;返回值被 resolve 包装
Promise.all 全 fulfilled 才 resolve;任一 reject 即 reject
allSettled 全部结束,单个失败不影响整体 settle
async/await 语法糖;并行用 Promise.all
LRU Map 删头插尾维护顺序;O(1) get/put
虚拟列表 只渲染可视区;start/end + 总高度占位

七、易混淆点归纳

  1. 「Promise 同步」:指 executor 同步,不是 then 同步。
  2. Promise.all vs race:all 等全部成功;race 谁先 settle 用谁(常用于超时)。
  3. catch 返回值:若返回普通值,后续 thenfulfilled 链。
  4. 循环 awaitfor + await 串行;勿误以为 async 函数会「自动并行」。
  5. LRU Map 法:面试够用;追问 O(1) 原理时再画双向链表 + 哈希表
  6. 虚拟列表:固定行高实现简单;动态行高需测量 + 缓存高度

八、思考与练习

1. 下面输出顺序是什么?

console.log("A");
Promise.resolve().then(() => console.log("B"));
console.log("C");

解析:A → C → Bthen 回调为微任务,在本轮同步代码之后执行。

2. Promise.all([p1, p2, p3])p2 先 reject,p1、p3 仍会执行完吗?

解析:(各 Promise 自身逻辑仍会走完),但 all 返回的 Promise 已 reject,结果通常被 catch 消费;与 allSettled「全部收尾」不同。

3. async function f() { return 1; } 的返回值类型是什么?

解析:Promise,等价 Promise.resolve(1)

4. LRU 容量 2,操作 put(1,1) put(2,2) put(1,1) put(3,3) get(2)get(2) 返回什么?

解析:-1put(1,1) 再次访问后 1 变为最新;put(3,3) 满容淘汰最久未用的 2

5. 1 万条数据列表卡顿,虚拟列表主要优化的是什么?

解析:DOM 节点数量与重排重绘范围;将渲染量从 O(N) 降到 O(可视行数)


总结

  • Promise:三态不可逆;手写 then 链、#resolvePromisecatchPromise.all;弄清 all 快速失败allSettled 区别。
  • async/await:Promise 语法糖;并行 Promise.all、串行 for-await;Generator + 自动执行器帮助理解原理。
  • LRUMap 维护顺序,满容删最旧;O(1) get/put。
  • 虚拟列表:按 scrollTop 算 start/end,占位总高度,只渲染可视区。

续篇将展开 Promise 与 async/await 专题(raceallSettledany、超时、重试、并发池等),并进入 数据结构基础 阶段。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐