手写实现 LazyMan(JS 流程控制经典面试题):链式调用、sleep 与优先级队列

手写实现 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 底层能力,这套模型都非常值得掌握。

记住:真正的高手,不只是写出能跑的代码,而是能在复杂场景下做出优雅的选择。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注