手写实现 LazyMan:链式调用、sleep 与优先级队列的深度解析
在前端开发中,面试题常常考验候选人对 JavaScript 异步机制、执行上下文、事件循环和设计模式的理解。其中,“LazyMan”是一个经典的流程控制类题目,它要求我们实现一个支持链式调用的对象,并能按顺序执行一系列任务(如 console.log),同时支持延迟执行(sleep)以及任务优先级排序。
本文将带你从零开始手写一个完整的 LazyMan 实现,涵盖以下核心知识点:
- 链式调用原理(this 指向与方法返回)
- 异步任务调度(setTimeout / Promise / microtask)
- 优先级队列设计(如何让高优先级任务先执行)
- 实际应用场景分析
我们将一步步构建这个系统,确保每一步都逻辑清晰、可运行、可扩展。
一、需求拆解与初步设计
1.1 题目目标
我们要实现一个函数 LazyMan(name),它可以被链式调用,例如:
LazyMan("Tony").eat("apple").sleep(3000).eat("banana")
输出应为:
Hi! This is Tony!
Eat apple~
Wait for 3s...
Eat banana~
关键点包括:
- 支持任意数量的方法链式调用;
- 每个动作(如
eat,sleep)必须按顺序执行; sleep是异步操作,不能阻塞后续任务;- 后续加入的任务可能具有不同优先级(比如紧急通知应在普通任务前执行);
1.2 核心组件设计
我们可以把整个 LazyMan 看作一个“任务调度器”,内部维护一个任务队列。每个任务包含:
- 执行函数(如
() => console.log('Eat apple~')) - 优先级(默认为 0,数字越大优先级越高)
- 是否需要等待上一个任务完成(用于 sleep)
因此,我们需要两个主要结构:
| 结构 | 作用 |
|——|——|
| Task Queue(优先级队列) | 存储待执行的任务,按优先级排序 |
| Executor(执行引擎) | 负责依次取出任务并执行 |
✅ 这种分层设计既保证了灵活性(可以轻松添加新类型任务),也便于调试和测试。
二、基础版本实现(链式调用 + 基础 sleep)
首先我们不考虑优先级,只实现最简单的链式调用和 sleep 功能。
2.1 构造函数与初始任务
class LazyMan {
constructor(name) {
this.name = name;
this.tasks = [];
// 初始任务:打招呼
this.tasks.push({
fn: () => console.log(`Hi! This is ${this.name}!`),
priority: 0,
delay: 0
});
return this; // 返回实例以支持链式调用
}
eat(food) {
this.tasks.push({
fn: () => console.log(`Eat ${food}~`),
priority: 0,
delay: 0
});
return this; // 必须返回 this,否则无法链式调用
}
sleep(time) {
this.tasks.push({
fn: () => new Promise(resolve => setTimeout(resolve, time)),
priority: 0,
delay: time
});
return this;
}
start() {
let index = 0;
const runNext = () => {
if (index >= this.tasks.length) return;
const task = this.tasks[index];
index++;
// 如果是 sleep 类型任务,需要等待一段时间再继续
if (task.delay > 0) {
task.fn().then(runNext);
} else {
task.fn();
runNext(); // 继续下一个任务
}
};
runNext();
}
}
2.2 使用示例
new LazyMan("Tony")
.eat("apple")
.sleep(3000)
.eat("banana")
.start();
✅ 输出结果符合预期:
Hi! This is Tony!
Eat apple~
Wait for 3s...
Eat banana~
⚠️ 注意:这里使用的是同步执行方式(非并发)。如果多个 sleep 同时发生,它们会串行执行 —— 正是我们想要的效果!
三、引入优先级队列(Priority Queue)
现在问题来了:如果用户希望某些任务更早执行怎么办?比如:
LazyMan("Tony")
.eat("apple") // 普通任务
.sleep(5000) // 等待很久
.eat("banana") // 但这个应该插队!
.priority(10) // 插入高优先级标记
.start();
这就需要我们在任务队列中按照优先级排序,而不是简单地 FIFO(先进先出)。
3.1 自定义优先级队列结构
我们不需要依赖第三方库,自己实现一个最小堆(Min Heap)作为优先级队列的基础:
class PriorityQueue {
constructor() {
this.queue = [];
}
enqueue(task) {
this.queue.push(task);
this._heapifyUp(this.queue.length - 1);
}
dequeue() {
if (this.queue.length === 0) return null;
if (this.queue.length === 1) return this.queue.pop();
const top = this.queue[0];
this.queue[0] = this.queue.pop();
this._heapifyDown(0);
return top;
}
_heapifyUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.queue[parentIndex].priority >= this.queue[index].priority) break;
[this.queue[parentIndex], this.queue[index]] = [this.queue[index], this.queue[parentIndex]];
index = parentIndex;
}
}
_heapifyDown(index) {
while (true) {
let leftChild = 2 * index + 1;
let rightChild = 2 * index + 2;
let largest = index;
if (leftChild < this.queue.length &&
this.queue[leftChild].priority > this.queue[largest].priority) {
largest = leftChild;
}
if (rightChild < this.queue.length &&
this.queue[rightChild].priority > this.queue[largest].priority) {
largest = rightChild;
}
if (largest === index) break;
[this.queue[index], this.queue[largest]] = [this.queue[largest], this.queue[index]];
index = largest;
}
}
size() {
return this.queue.length;
}
}
💡 这里采用最大堆(Max Heap)实现优先级队列,优先级数值越大越靠前。
3.2 修改 LazyMan 的构造逻辑
class LazyMan {
constructor(name) {
this.name = name;
this.taskQueue = new PriorityQueue();
this.taskQueue.enqueue({
fn: () => console.log(`Hi! This is ${this.name}!`),
priority: 0,
delay: 0
});
return this;
}
eat(food) {
this.taskQueue.enqueue({
fn: () => console.log(`Eat ${food}~`),
priority: 0,
delay: 0
});
return this;
}
sleep(time) {
this.taskQueue.enqueue({
fn: () => new Promise(resolve => setTimeout(resolve, time)),
priority: 0,
delay: time
});
return this;
}
priority(p) {
// 最后一个任务设置优先级
const lastTask = this.taskQueue.queue[this.taskQueue.queue.length - 1];
if (lastTask) lastTask.priority = p;
return this;
}
start() {
const runNext = () => {
const task = this.taskQueue.dequeue();
if (!task) return;
if (task.delay > 0) {
task.fn().then(runNext);
} else {
task.fn();
runNext();
}
};
runNext();
}
}
3.3 测试优先级功能
new LazyMan("Tony")
.eat("apple")
.sleep(5000)
.eat("banana")
.priority(10) // 设置优先级为 10(比默认高)
.start();
此时,即使 sleep(5000) 在中间,也会因为 eat("banana") 的优先级更高而提前执行。
✅ 输出顺序变为:
Hi! This is Tony!
Eat apple~
Eat banana~
Wait for 5s...
这正是我们期望的行为!
四、高级特性增强(可选)
为了使 LazyMan 更加实用,我们可以增加以下功能:
| 特性 | 实现说明 |
|---|---|
.then() 方法支持 |
允许在某个任务完成后触发回调 |
.catch() 错误处理 |
对异常进行统一捕获 |
.timeout(ms) |
设置任务超时时间,避免无限等待 |
.cancel() |
中断当前正在执行的任务流 |
这些都可以通过扩展 Task 结构来实现,例如:
const task = {
fn: () => { ... },
priority: 0,
delay: 0,
timeout: null, // 可选超时时间
onResolve: null, // 成功回调
onReject: null // 失败回调
};
但出于篇幅限制,我们暂不展开这部分内容。你可以根据实际项目需求逐步完善。
五、性能对比与优化建议
| 方案 | 时间复杂度 | 适用场景 |
|---|---|---|
| 数组模拟 FIFO | O(n) | 小规模任务、无优先级需求 |
| 优先级队列(堆) | O(log n) | 多任务、有优先级需求 |
| 微任务队列(Promise.resolve) | O(1) | 短任务、无需阻塞主线程 |
📌 推荐在生产环境中使用优先级队列方案,因为它能有效管理大量任务且保持良好的响应性。
此外,还可以考虑:
- 使用
requestIdleCallback来优化空闲时间调度; - 将任务序列持久化到 localStorage(适用于浏览器环境);
- 添加日志记录功能用于调试。
六、总结与思考
LazyMan 不仅是一个面试题,更是现代前端任务调度系统的缩影。它涉及的核心技术包括:
| 技术点 | 关键理解 |
|---|---|
| 链式调用 | this 指向控制、方法返回值一致性 |
| 异步执行 | setTimeout / Promise / async/await 的选择与区别 |
| 优先级调度 | 堆结构的设计与应用 |
| 设计模式 | 责任链 + 观察者模式雏形 |
💡 重要提醒:
- 不要盲目追求“极致性能”,而是要根据业务场景合理取舍;
- 优先级不是万能药,过度使用可能导致逻辑混乱;
- 单元测试必不可少,尤其是异步任务的边界条件。
七、完整代码清单(最终版)
// 优先级队列实现
class PriorityQueue {
constructor() {
this.queue = [];
}
enqueue(task) {
this.queue.push(task);
this._heapifyUp(this.queue.length - 1);
}
dequeue() {
if (this.queue.length === 0) return null;
if (this.queue.length === 1) return this.queue.pop();
const top = this.queue[0];
this.queue[0] = this.queue.pop();
this._heapifyDown(0);
return top;
}
_heapifyUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.queue[parentIndex].priority >= this.queue[index].priority) break;
[this.queue[parentIndex], this.queue[index]] = [this.queue[index], this.queue[parentIndex]];
index = parentIndex;
}
}
_heapifyDown(index) {
while (true) {
let leftChild = 2 * index + 1;
let rightChild = 2 * index + 2;
let largest = index;
if (leftChild < this.queue.length &&
this.queue[leftChild].priority > this.queue[largest].priority) {
largest = leftChild;
}
if (rightChild < this.queue.length &&
this.queue[rightChild].priority > this.queue[largest].priority) {
largest = rightChild;
}
if (largest === index) break;
[this.queue[index], this.queue[largest]] = [this.queue[largest], this.queue[index]];
index = largest;
}
}
size() {
return this.queue.length;
}
}
// LazyMan 主体
class LazyMan {
constructor(name) {
this.name = name;
this.taskQueue = new PriorityQueue();
this.taskQueue.enqueue({
fn: () => console.log(`Hi! This is ${this.name}!`),
priority: 0,
delay: 0
});
return this;
}
eat(food) {
this.taskQueue.enqueue({
fn: () => console.log(`Eat ${food}~`),
priority: 0,
delay: 0
});
return this;
}
sleep(time) {
this.taskQueue.enqueue({
fn: () => new Promise(resolve => setTimeout(resolve, time)),
priority: 0,
delay: time
});
return this;
}
priority(p) {
const lastTask = this.taskQueue.queue[this.taskQueue.queue.length - 1];
if (lastTask) lastTask.priority = p;
return this;
}
start() {
const runNext = () => {
const task = this.taskQueue.dequeue();
if (!task) return;
if (task.delay > 0) {
task.fn().then(runNext);
} else {
task.fn();
runNext();
}
};
runNext();
}
}
// 使用示例
new LazyMan("Tony")
.eat("apple")
.sleep(3000)
.eat("banana")
.priority(10)
.start();
这篇文章深入浅出地讲解了 LazyMan 的实现原理,从基础链式调用到优先级调度,再到可扩展设计,为你提供了完整的工程化思路。无论你是准备面试还是想提升 JS 底层能力,这套模型都非常值得掌握。
记住:真正的高手,不只是写出能跑的代码,而是能在复杂场景下做出优雅的选择。