前端秋招异步手写题系统刷题路线:从 Promise 到 Pipeline、LazyMan 与并发控制

适用场景:前端秋招 / 实习面试 / JavaScript 手写题

核心目标:不要死记每一道题,而是掌握背后的几种通用模型。


一、为什么要单独整理"异步手写题"?

前端面试中的很多 JavaScript 手写题,看起来形式完全不同:

  • sleep
  • Promise.all
  • Promise 串行执行
  • Pipeline
  • TaskQueue
  • LazyMan
  • Scheduler
  • 并发限制
  • retry
  • timeout
  • 请求去重

但本质上,这些题并不是十几个独立知识点。

它们大部分都建立在以下几个核心能力之上:

text 复制代码
Promise
  ↓
async / await
  ↓
异步任务串行执行
  ↓
任务队列
  ↓
链式调用
  ↓
Pipeline / LazyMan
  ↓
并发调度

因此,正确的刷题方法不是:

text 复制代码
背 50 道题

而是:

text 复制代码
掌握 5~6 个母模板
↓
看到题目识别模型
↓
根据需求做变形

本文按照由浅入深的方式,整理一套适合前端秋招的 7 天 / 12 题刷题路线


二、整体刷题路线

建议按照下面的顺序刷:

text 复制代码
Day 1:Promise 基础
  ① sleep
  ② Promise.all

Day 2:异步串行
  ③ 多任务串行执行
  ④ 上一步输出作为下一步输入

Day 3:异常控制
  ⑤ timeout
  ⑥ retry

Day 4:任务队列
  ⑦ TaskQueue
  ⑧ Pipeline

Day 5:链式异步
  ⑨ LazyMan / CodingMan

Day 6:并发控制
  ⑩ Scheduler
  ⑪ mapLimit

Day 7:综合工程题
  ⑫ 请求去重

三、Day 1:Promise 基础


1. 实现 sleep

题目

实现下面的效果:

js 复制代码
await sleep(2000);

console.log('2 秒后输出');

思路分析

sleep 的本质非常简单:

text 复制代码
setTimeout
   ↓
时间到了
   ↓
resolve()

所以:

js 复制代码
new Promise(resolve => {
  setTimeout(resolve, ms);
});

参考答案

js 复制代码
function sleep(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}

使用:

js 复制代码
async function test() {
  console.log('start');

  await sleep(2000);

  console.log('end');
}

test();

速记

text 复制代码
sleep
=
setTimeout
+
Promise

四、实现 Promise.all

2. 手写 Promise.all

题目

实现:

js 复制代码
myPromiseAll([
  Promise.resolve(1),
  Promise.resolve(2),
  3
]);

最终结果:

js 复制代码
[1, 2, 3]

思路分析

需要解决三个问题:

text 复制代码
① 所有 Promise 可以并行执行

② 保存每一个 Promise 的执行结果

③ 所有任务完成后统一 resolve

其中最容易出错的是:

text 复制代码
Promise 完成顺序
≠
最终返回结果顺序

例如:

text 复制代码
任务 A:3 秒
任务 B:1 秒
任务 C:2 秒

实际完成顺序:

text 复制代码
B → C → A

但最终结果依然必须是:

text 复制代码
[A, B, C]

所以不能简单使用:

js 复制代码
result.push(value);

应该使用:

js 复制代码
result[index] = value;

参考答案

js 复制代码
function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    const result = [];
    let count = 0;

    if (promises.length === 0) {
      resolve([]);
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => {
          result[index] = value;
          count++;

          if (count === promises.length) {
            resolve(result);
          }
        })
        .catch(reject);
    });
  });
}

核心考点

text 复制代码
Promise.resolve()
→ 兼容普通值和 Promise

result[index]
→ 保证结果顺序

count
→ 判断是否全部完成

reject
→ 任意一个失败就失败

五、Day 2:异步串行


3. 多个异步任务串行执行

题目

给定:

js 复制代码
const tasks = [
  () => Promise.resolve(1),
  () => Promise.resolve(2),
  () => Promise.resolve(3)
];

要求执行顺序:

text 复制代码
task1
↓
task2
↓
task3

思路分析

最简单的方式就是:

js 复制代码
for...of + await

因为:

js 复制代码
await task();

当前任务没有执行结束之前,不会进入下一轮循环。


参考答案

js 复制代码
async function runTasks(tasks) {
  const result = [];

  for (const task of tasks) {
    const value = await task();

    result.push(value);
  }

  return result;
}

执行模型

text 复制代码
for
 ↓
await task1
 ↓
await task2
 ↓
await task3

六、上一步输出作为下一步输入

4. 数据流水线

题目

给定:

js 复制代码
const tasks = [
  async value => value + 1,
  async value => value * 2,
  async value => value - 3
];

输入:

js 复制代码
10

要求:

text 复制代码
10
↓ +1
11
↓ ×2
22
↓ -3
19

思路分析

只需要维护一个变量:

js 复制代码
value

每一次执行:

js 复制代码
value = await task(value);

上一步的输出,就成为下一步的输入。


参考答案

js 复制代码
async function runTasks(tasks, initialValue) {
  let value = initialValue;

  for (const task of tasks) {
    value = await task(value);
  }

  return value;
}

最核心的一行

js 复制代码
value = await task(value);

实际上可以拆成:

js 复制代码
const result = await task(value);

value = result;

所以整个数据流是:

text 复制代码
value0
  ↓
task1
  ↓
value1
  ↓
task2
  ↓
value2
  ↓
task3
  ↓
value3

这个模型就是后面 Pipeline 的核心。


七、Day 3:异常与失败控制


5. 实现 Promise timeout

题目

实现:

js 复制代码
timeout(fetchData(), 3000);

要求:

text 复制代码
3 秒内完成
→ 返回正常结果

超过 3 秒
→ reject

思路分析

这里其实是在进行一场竞赛:

text 复制代码
正常任务 Promise
      VS
超时 Promise

谁先完成,就采用谁的结果。

因此天然想到:

js 复制代码
Promise.race()

参考答案

js 复制代码
function timeout(promise, ms) {
  const timer = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('timeout'));
    }, ms);
  });

  return Promise.race([
    promise,
    timer
  ]);
}

核心模型

text 复制代码
Promise.race([
  正常任务,
  超时任务
])

八、实现 retry

6. 失败自动重试

题目

实现:

js 复制代码
retry(task, 3);

任务失败时进行重试。


思路分析

本质是:

text 复制代码
执行任务
 ↓
成功
→ return

失败
 ↓
还有机会?
 ↓
继续执行

所以核心是:

text 复制代码
for
+
try / catch

参考答案

js 复制代码
async function retry(task, times) {
  let lastError;

  for (let i = 0; i < times; i++) {
    try {
      return await task();
    } catch (error) {
      lastError = error;
    }
  }

  throw lastError;
}

面试容易追问

下面这句:

js 复制代码
retry(task, 3);

到底代表:

text 复制代码
总共执行 3 次

还是:

text 复制代码
第一次执行
+
失败后再重试 3 次

总共 4 次

取决于题目定义。

实际工程中,更建议使用:

js 复制代码
maxAttempts

这种参数名,避免语义歧义。


九、Day 4:任务队列


7. 实现 TaskQueue

题目

实现:

js 复制代码
queue.add(task1);
queue.add(task2);
queue.add(task3);

queue.run();

要求:

text 复制代码
task1
↓
task2
↓
task3

思路分析

这里要分清两个阶段。

第一阶段:添加任务

js 复制代码
this.tasks.push(task);

只是在:

text 复制代码
保存任务

并不是立即执行。

第二阶段:执行任务

js 复制代码
for...of + await

负责真正消费队列。


参考答案

js 复制代码
class TaskQueue {
  constructor() {
    this.tasks = [];
  }

  add(task) {
    this.tasks.push(task);

    return this;
  }

  async run() {
    for (const task of this.tasks) {
      await task();
    }
  }
}

核心模型

text 复制代码
add()
 ↓
任务入队

run()
 ↓
消费任务

十、实现 Pipeline

8. Pipeline

题目

实现:

js 复制代码
const pipeline = new Pipeline();

pipeline
  .add(async value => value + 1)
  .add(async value => value * 2)
  .add(async value => value - 3);

const result = await pipeline.run(10);

最终:

js 复制代码
result === 19

思路分析

Pipeline 实际上是在 TaskQueue 基础上增加了两个能力。


第一:支持链式调用

js 复制代码
pipeline
  .add(...)
  .add(...)
  .add(...);

实现方式:

js 复制代码
return this;

第二:传递上一步结果

text 复制代码
上一个任务输出
↓
下一个任务输入

所以:

js 复制代码
value = await handler(value);

参考答案

js 复制代码
class Pipeline {
  constructor() {
    this.handlers = [];
  }

  add(handler) {
    if (typeof handler !== 'function') {
      throw new TypeError('handler must be a function');
    }

    this.handlers.push(handler);

    return this;
  }

  async run(initialValue) {
    let value = initialValue;

    for (const handler of this.handlers) {
      value = await handler(value);
    }

    return value;
  }
}

Pipeline 四个核心考点

text 复制代码
handlers
→ 保存任务

push()
→ 任务入队

return this
→ 链式调用

value = await handler(value)
→ 串行执行 + 数据传递

Pipeline 真正需要理解的,其实就是最后一句:

js 复制代码
value = await handler(value);

十一、Pipeline 的 cancel 怎么理解?

有些题会要求:

js 复制代码
pipeline.cancel();

一种简单写法:

js 复制代码
class Pipeline {
  constructor() {
    this.handlers = [];
    this._canceled = false;
  }

  cancel() {
    this._canceled = true;
  }
}

然后:

js 复制代码
for (const handler of this.handlers) {
  if (this._canceled) {
    throw new Error('Pipeline canceled');
  }

  value = await handler(value);
}

需要注意:

js 复制代码
if (this._canceled)

不是实时监听。

它只是在代码执行到这一行的时候检查一次。

所以:

text 复制代码
task1 正在执行
↓
调用 cancel()
↓
task1 不会立即停止
↓
task1 执行结束
↓
下一轮循环发现 canceled
↓
停止后续任务

因此这种 cancel 更准确的含义是:

阻止后续任务继续执行。

如果想真正取消当前 fetch 等异步任务,需要进一步使用:

text 复制代码
AbortController
+
AbortSignal

但对于普通秋招手写题,这已经属于进阶扩展,理解概念即可。


十二、Day 5:链式异步调用


9. 实现 LazyMan

题目

实现:

js 复制代码
LazyMan('Tom')
  .sleep(3)
  .eat('apple')
  .sleep(2)
  .eat('banana');

期望执行:

text 复制代码
Hi Tom

等待 3 秒

Eat apple

等待 2 秒

Eat banana

思路分析

重点:

调用:

js 复制代码
sleep()

时不要立刻 sleep。

而是:

text 复制代码
调用 sleep
↓
创建任务函数
↓
任务入队
↓
最后统一执行

所以 LazyMan 和 Pipeline 的本质其实非常接近。


参考答案

js 复制代码
class LazyManClass {
  constructor(name) {
    this.tasks = [];

    this.tasks.push(async () => {
      console.log(`Hi ${name}`);
    });

    Promise.resolve().then(() => {
      this.run();
    });
  }

  eat(food) {
    this.tasks.push(async () => {
      console.log(`Eat ${food}`);
    });

    return this;
  }

  sleep(seconds) {
    this.tasks.push(async () => {
      await new Promise(resolve => {
        setTimeout(resolve, seconds * 1000);
      });
    });

    return this;
  }

  async run() {
    for (const task of this.tasks) {
      await task();
    }
  }
}

function LazyMan(name) {
  return new LazyManClass(name);
}

Pipeline 与 LazyMan 的区别

Pipeline

text 复制代码
value
 ↓
task1
 ↓
value
 ↓
task2
 ↓
value

需要传递结果。

LazyMan

text 复制代码
task1
↓
task2
↓
task3

通常不需要传递 value。


二者共同点

text 复制代码
任务数组
+
任务入队
+
return this
+
for...of + await

所以它们其实属于同一类题。


十三、Day 6:并发控制

并发控制是异步手写题里非常重要的一层。


10. 实现 Scheduler

题目

假设:

js 复制代码
const scheduler = new Scheduler(2);

scheduler.add(task1);
scheduler.add(task2);
scheduler.add(task3);
scheduler.add(task4);

要求任何时刻最多只能同时执行:

text 复制代码
2 个任务

思路分析

维护两个状态:

js 复制代码
queue

表示:

text 复制代码
还没执行的任务

以及:

js 复制代码
runningCount

表示:

text 复制代码
当前正在执行的任务数量

核心条件:

js 复制代码
runningCount < limit

如果满足:

text 复制代码
可以继续启动任务

否则:

text 复制代码
必须等待

参考答案

js 复制代码
class Scheduler {
  constructor(limit) {
    this.limit = limit;
    this.runningCount = 0;
    this.queue = [];
  }

  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        task,
        resolve,
        reject
      });

      this.run();
    });
  }

  run() {
    while (
      this.runningCount < this.limit &&
      this.queue.length > 0
    ) {
      const {
        task,
        resolve,
        reject
      } = this.queue.shift();

      this.runningCount++;

      Promise.resolve()
        .then(task)
        .then(resolve)
        .catch(reject)
        .finally(() => {
          this.runningCount--;

          this.run();
        });
    }
  }
}

执行过程

假设:

js 复制代码
limit = 2;

初始:

text 复制代码
queue:

task1
task2
task3
task4

running = 0

首先执行:

text 复制代码
task1
task2

此时:

text 复制代码
running = 2

task3 和 task4 等待。

当 task2 完成:

text 复制代码
running = 1

Scheduler 立刻发现:

text 复制代码
running < limit

于是启动:

text 复制代码
task3

又变成:

text 复制代码
running = 2

Scheduler 核心

js 复制代码
this.runningCount++;

表示:

text 复制代码
占用一个并发槽位

任务结束后:

js 复制代码
this.runningCount--;

表示:

text 复制代码
释放一个并发槽位

然后:

js 复制代码
this.run();

继续调度下一个任务。


十四、实现 mapLimit

11. 限制并发数量

题目

实现:

js 复制代码
await mapLimit(
  [1, 2, 3, 4, 5],
  2,
  async item => {
    return fetchSomething(item);
  }
);

要求:

text 复制代码
最多同时执行 2 个任务

最终返回:

js 复制代码
[
  result1,
  result2,
  result3,
  result4,
  result5
]

思路分析

一个很好理解的方法是:

创建固定数量的 worker。

例如:

js 复制代码
limit = 2;

那么创建:

text 复制代码
worker1
worker2

两个 worker 不断从任务池中领取任务。


参考答案

js 复制代码
async function mapLimit(items, limit, fn) {
  const result = new Array(items.length);

  let index = 0;

  async function worker() {
    while (index < items.length) {
      const currentIndex = index++;

      result[currentIndex] = await fn(
        items[currentIndex]
      );
    }
  }

  const workers = [];

  for (
    let i = 0;
    i < Math.min(limit, items.length);
    i++
  ) {
    workers.push(worker());
  }

  await Promise.all(workers);

  return result;
}

执行示意

假设:

text 复制代码
5 个任务

并发数 = 2

可以理解为:

text 复制代码
worker1:
task1
↓
task3
↓
task5


worker2:
task2
↓
task4

两个 worker 同时工作。

因此同时最多只有:

text 复制代码
2 个异步任务

十五、Day 7:综合工程题


12. 请求去重

题目

如果连续调用:

js 复制代码
request('/user/1');

request('/user/1');

request('/user/1');

不希望发送三次相同请求。

而应该:

text 复制代码
第一次
→ 真正发送请求

第二次
→ 复用第一次 Promise

第三次
→ 继续复用第一次 Promise

思路分析

最适合的数据结构:

js 复制代码
Map

保存:

text 复制代码
请求 key
↓
正在执行的 Promise

参考答案

js 复制代码
const pendingRequests = new Map();

function request(url) {
  if (pendingRequests.has(url)) {
    return pendingRequests.get(url);
  }

  const promise = fetch(url)
    .finally(() => {
      pendingRequests.delete(url);
    });

  pendingRequests.set(url, promise);

  return promise;
}

为什么 finally 里要 delete?

请求执行期间:

text 复制代码
pendingRequests

/user/1
↓
Promise

可以让后续相同请求直接复用。

但请求结束以后,这个 Promise 就没有继续缓存的必要。

所以:

js 复制代码
finally(() => {
  pendingRequests.delete(url);
});

否则以后所有:

js 复制代码
request('/user/1');

都会一直拿到旧的 Promise。


十六、真正需要掌握的 6 个母模板

刷完 12 道题后,不建议背 12 套代码。

真正需要记住的是下面 6 个模型。


模板 1:延迟模型

js 复制代码
new Promise(resolve => {
  setTimeout(resolve, ms);
});

对应:

text 复制代码
sleep
delay
timeout

模板 2:串行模型

js 复制代码
for (const task of tasks) {
  await task();
}

对应:

text 复制代码
Promise 串行
TaskQueue
LazyMan

模板 3:数据流水线模型

js 复制代码
let value = initialValue;

for (const task of tasks) {
  value = await task(value);
}

对应:

text 复制代码
Pipeline
composeAsync
数据处理流水线

模板 4:链式调用模型

js 复制代码
add(task) {
  this.tasks.push(task);

  return this;
}

对应:

text 复制代码
Pipeline
LazyMan
Builder

模板 5:并发控制模型

核心思想:

js 复制代码
if (running < limit) {
  running++;

  task().finally(() => {
    running--;

    runNext();
  });
}

对应:

text 复制代码
Scheduler
并发请求
请求池
任务调度

模板 6:请求缓存 / 去重模型

js 复制代码
const map = new Map();

if (map.has(key)) {
  return map.get(key);
}

const promise = request();

map.set(key, promise);

对应:

text 复制代码
请求去重
资源加载去重
Promise 缓存

十七、正确刷题方式

一道题真正掌握的标准,不是:

我看懂答案了。

而是:

我看到题目以后,知道它属于哪个模型。

建议每道题严格执行下面的流程。


第一遍:只看题目

自己写:

text 复制代码
15~20 分钟

先不要看答案。


第二遍:只看思路

如果写不出来:

text 复制代码
只看解题思路

仍然不要看完整代码。


第三遍:继续独立实现

根据思路重新写一遍。


第四遍:对照答案

重点找:

text 复制代码
变量设计是否不同?

异常是否处理?

边界条件是否漏掉?

有没有更简洁的模型?

第五遍:关闭答案

重新完整手写。


第二天:再次手写

真正有效的复习是:

text 复制代码
隔天重新写

而不是当天连续看五遍。


十八、秋招优先级

如果时间有限,可以按照下面的优先级准备。


第一优先级:必须熟练

text 复制代码
★★★★★

Promise.all

sleep

Promise 串行

Pipeline

LazyMan

Scheduler

并发限制

这些题型之间关联非常强。


第二优先级:必须会

text 复制代码
★★★★

retry

timeout

EventEmitter

debounce

throttle

第三优先级:进阶

text 复制代码
★★★

请求去重

缓存

AbortController

完整 Promise 实现

完整手写 Promise 的代码量比较大,实际秋招准备中优先级通常没有:

text 复制代码
Promise.all
Scheduler
并发控制

那么高。


十九、完整知识演化路线

最终建议建立下面这条知识线:

text 复制代码
Promise
   ↓
async / await
   ↓
异步串行
   ↓
TaskQueue
   ↓
Pipeline
   ↓
LazyMan
   ↓
Scheduler
   ↓
并发控制

这几道题并不是孤立的。


Promise 串行

js 复制代码
for (const task of tasks) {
  await task();
}

加入结果传递:

js 复制代码
value = await task(value);

就变成:

text 复制代码
Pipeline

再加入:

js 复制代码
return this;

就可以支持:

text 复制代码
链式调用

再把各种:

text 复制代码
eat
sleep
sleepFirst

包装成任务:

text 复制代码
LazyMan

再增加:

text 复制代码
running
limit
queue

就进入:

text 复制代码
Scheduler / 并发控制

所以整个体系实际上是在不断增加状态和约束。


二十、最后总结

前端异步手写题最重要的不是背代码,而是识别模式。

建议牢牢记住:

text 复制代码
sleep
→ setTimeout + Promise

串行
→ for...of + await

Pipeline
→ value = await task(value)

链式调用
→ return this

LazyMan
→ 任务入队 + 延迟统一执行

Scheduler
→ queue + running + limit

请求去重
→ Map + Promise 缓存

最终目标应该是:

text 复制代码
看到题目
↓
判断属于哪个模型
↓
写出骨架
↓
再补边界条件

如果能做到这一点,绝大多数前端秋招里的异步手写题,就不再是"新题",而只是这些基本模型的不同组合。


推荐刷题顺序

最后再浓缩成一版:

text 复制代码
1. sleep
2. Promise.all
3. Promise 串行
4. 上一步结果传给下一步
5. timeout
6. retry
7. TaskQueue
8. Pipeline
9. LazyMan
10. Scheduler
11. mapLimit
12. 请求去重

先把这 12 道题做到:

text 复制代码
不看答案能独立写出 8~10 道

再继续补:

text 复制代码
EventEmitter
debounce
throttle
call
apply
bind
new
instanceof
deepClone

这样就可以逐步形成一套完整的前端秋招 JavaScript 手写题体系。

相关推荐
秋天的一阵风1 小时前
⚡上线 24 小时,13% 的付费团队连夜换到 Jev:它到底什么来头?
前端·人工智能·ai编程
程序员若风1 小时前
CSRF攻击原理介绍和利用-腾讯云开发者社区
前端·网络·安全·web安全·网络安全·csrf攻击·腾讯云开发者社区
风尘小子1 小时前
node.js系列:process配置
前端·node.js
恋猫de小郭1 小时前
AndroidX 新增 Security State ,支持可编程的系统安全状态查询
android·前端·flutter
IT_陈寒2 小时前
Redis大key删除引发的服务雪崩,这次我真记住了
前端·人工智能·后端
IMPYLH2 小时前
HTML 的 <style> 元素
前端·html
paopaokaka_luck2 小时前
考研政治刷题小程序(AI推荐题目、ECharts数据分析、章节练习与专项训练、模拟考试、错题本与收藏夹、学习打卡与目标管理、题库和组卷维护)
前端·javascript·spring boot·spring·数据分析·echarts
刃神太酷啦2 小时前
前端入门第一课:HTML 基础语法 + 常用标签 + 实战全解
服务器·c语言·前端·javascript·css·c++·html
mldong2 小时前
给流程设计器加国际化,我没引 vue-i18n
前端·javascript·vue.js