【前端面试小册】JS-第16节:实现 Lazy 链式调用(百度二面)

一、题目描述

1.1 需求分析

实现一个 lazyMan 函数,要求:

  1. 能够链式调用:支持连续调用多个方法
  2. firstSleep 优先执行:无论 firstSleep 在什么位置都最先调用
  3. 顺序执行:当 firstSleep 执行完毕后,后面按照链式调用的顺序执行
  4. 延迟阻塞:遇到 sleep 延迟函数,会阻碍函数继续执行,必须得等延迟时间结束后继续按顺序执行

1.2 示例

lazyMan('Lazy')
    .eat('apple')
    .firstSleep(1)
    .eat('banner')
    .sleep(2)
    .eat('orange');

// 会输出如下:
// 【 Lazy 】:第1次睡觉
// 【 Lazy 】:第1次睡醒了
// -----分割线-----
// 【 Lazy 】:正在吃apple
// 【 Lazy 】:正在吃banner
// 【 Lazy 】:第2次睡觉
// 【 Lazy 】:第2次睡醒
// 【 Lazy 】:正在吃orange

执行顺序分析

graph TD
    A[lazyMan 初始化] --> B[eat apple 入队]
    B --> C[firstSleep 1秒 优先入队]
    C --> D[eat banner 入队]
    D --> E[sleep 2秒 入队]
    E --> F[eat orange 入队]
    F --> G[开始执行队列]
    G --> H[firstSleep 执行]
    H --> I[输出分割线]
    I --> J[eat apple 执行]
    J --> K[eat banner 执行]
    K --> L[sleep 2秒 执行]
    L --> M[eat orange 执行]

二、实现思路

2.1 核心思想

  1. 任务队列:使用数组 fun_stack 存储所有待执行的任务
  2. Promise 链:使用 Promise 链确保任务按顺序执行
  3. 优先级处理firstSleep 使用 unshift 插入队列头部,其他使用 push 插入队列尾部
  4. 异步执行:使用 setTimeout 确保所有任务入队后再开始执行

2.2 实现方案

function lazyMan(name) {
    return new _lazyMan(name);
}

function _lazyMan(name) {
    let flag = Promise.resolve();  // Promise 链的起始点
    this.name = name;
    this.fun_stack = [];  // 任务队列
    const self = this;
    
    // 添加分割线函数
    function fn() {
        console.log(`-----分割线-----`);
        return Promise.resolve();
    }
    this.fun_stack.push(fn);

    // 使用 setTimeout 确保所有任务入队后再执行
    setTimeout(function() {
        self.fun_stack.forEach(fn => {
            flag = flag.then(fn);  // 链式执行 Promise
        });
    });
}

// sleep 方法:延迟执行
_lazyMan.prototype.sleep = function(time) {
    const self = this;
    function fn() {
        return new Promise(function(resolve) {
            console.log(`【 ${self.name} 】:第2次睡觉`);
            setTimeout(function() {
                console.log(`【 ${self.name} 】:第2次睡醒`);
                resolve();
            }, time * 1000);
        });
    }
    this.fun_stack.push(fn);  // 添加到队列尾部
    return this;  // 返回 this 支持链式调用
};

// firstSleep 方法:优先执行
_lazyMan.prototype.firstSleep = function(time) {
    const self = this;
    function fn() {
        return new Promise(function(resolve) {
            console.log(`【 ${self.name} 】:第1次睡觉`);
            setTimeout(function() {
                console.log(`【 ${self.name} 】:第1次睡醒了`);
                resolve();
            }, time * 1000);
        });
    }
    this.fun_stack.unshift(fn);  // 添加到队列头部,优先执行
    return this;  // 返回 this 支持链式调用
};

// eat 方法:立即执行
_lazyMan.prototype.eat = function(food) {
    const self = this;
    function fn() {
        console.log(`【 ${self.name} 】:正在吃${food}`);
        return Promise.resolve();
    }
    this.fun_stack.push(fn);  // 添加到队列尾部
    return this;  // 返回 this 支持链式调用
};

三、代码详解

3.1 初始化阶段

function _lazyMan(name) {
    let flag = Promise.resolve();  // Promise 链的起始点
    this.name = name;
    this.fun_stack = [];  // 任务队列
    const self = this;
    
    // 添加分割线函数
    function fn() {
        console.log(`-----分割线-----`);
        return Promise.resolve();
    }
    this.fun_stack.push(fn);

    // 使用 setTimeout 确保所有任务入队后再执行
    setTimeout(function() {
        self.fun_stack.forEach(fn => {
            flag = flag.then(fn);  // 链式执行 Promise
        });
    });
}

关键点

  • flag:Promise 链的起始点,用于串联所有任务
  • fun_stack:任务队列,存储所有待执行的任务
  • setTimeout:确保所有同步代码执行完毕,所有任务都已入队后再开始执行

3.2 Promise 链执行机制

// Promise 链的执行过程
let flag = Promise.resolve();

// 第一个任务
flag = flag.then(fn1);  // fn1 执行完后,flag 指向新的 Promise

// 第二个任务
flag = flag.then(fn2);  // fn2 等待 fn1 完成后执行

// 第三个任务
flag = flag.then(fn3);  // fn3 等待 fn2 完成后执行

执行流程

graph TD
    A[Promise.resolve] --> B[flag = flag.then fn1]
    B --> C[fn1 执行]
    C --> D[flag = flag.then fn2]
    D --> E[fn2 等待 fn1]
    E --> F[fn1 完成]
    F --> G[fn2 执行]
    G --> H[flag = flag.then fn3]
    H --> I[fn3 等待 fn2]
    I --> J[fn2 完成]
    J --> K[fn3 执行]

3.3 firstSleep 优先执行原理

_lazyMan.prototype.firstSleep = function(time) {
    // ...
    this.fun_stack.unshift(fn);  // 使用 unshift 插入队列头部
    return this;
};

unshift vs push

// 假设队列初始状态:[分割线]
// 调用顺序:eat('apple') -> firstSleep(1) -> eat('banner')

// 使用 push(错误)
fun_stack.push(eat_apple);      // [分割线, eat_apple]
fun_stack.push(firstSleep);     // [分割线, eat_apple, firstSleep]
fun_stack.push(eat_banner);     // [分割线, eat_apple, firstSleep, eat_banner]
// 执行顺序:分割线 -> eat_apple -> firstSleep -> eat_banner(错误)

// 使用 unshift(正确)
fun_stack.push(eat_apple);      // [分割线, eat_apple]
fun_stack.unshift(firstSleep);  // [firstSleep, 分割线, eat_apple]
fun_stack.push(eat_banner);     // [firstSleep, 分割线, eat_apple, eat_banner]
// 执行顺序:firstSleep -> 分割线 -> eat_apple -> eat_banner(正确)

3.4 sleep 延迟阻塞原理

_lazyMan.prototype.sleep = function(time) {
    function fn() {
        return new Promise(function(resolve) {
            console.log(`【 ${self.name} 】:第2次睡觉`);
            setTimeout(function() {
                console.log(`【 ${self.name} 】:第2次睡醒`);
                resolve();  // 延迟结束后 resolve,继续执行下一个任务
            }, time * 1000);
        });
    }
    this.fun_stack.push(fn);
    return this;
};

关键点

  • 返回一个 Promise,在 setTimeout 的回调中 resolve
  • 下一个任务会等待这个 Promise 完成后再执行
  • 实现了延迟阻塞的效果

四、完整示例

4.1 基础示例

lazyMan('Lazy')
    .eat('apple')
    .firstSleep(1)
    .eat('banner')
    .sleep(2)
    .eat('orange');

// 输出:
// 【 Lazy 】:第1次睡觉
// 【 Lazy 】:第1次睡醒了
// -----分割线-----
// 【 Lazy 】:正在吃apple
// 【 Lazy 】:正在吃banner
// 【 Lazy 】:第2次睡觉
// 【 Lazy 】:第2次睡醒
// 【 Lazy 】:正在吃orange

4.2 复杂示例

lazyMan('Tom')
    .eat('breakfast')
    .firstSleep(2)
    .eat('lunch')
    .sleep(3)
    .eat('dinner')
    .sleep(1)
    .eat('snack');

// 执行顺序:
// 1. firstSleep(2) - 优先执行,延迟 2 秒
// 2. 分割线
// 3. eat('breakfast') - 立即执行
// 4. eat('lunch') - 立即执行
// 5. sleep(3) - 延迟 3 秒
// 6. eat('dinner') - 延迟后执行
// 7. sleep(1) - 延迟 1 秒
// 8. eat('snack') - 延迟后执行

五、优化版本

5.1 使用 ES6 语法优化

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

class LazyMan {
    constructor(name) {
        this.name = name;
        this.taskQueue = [];
        this.promiseChain = Promise.resolve();
        
        // 添加分割线
        this.taskQueue.push(() => {
            console.log('-----分割线-----');
            return Promise.resolve();
        });
        
        // 确保所有任务入队后再执行
        setTimeout(() => {
            this.taskQueue.forEach(task => {
                this.promiseChain = this.promiseChain.then(task);
            });
        });
    }
    
    sleep(time) {
        this.taskQueue.push(() => {
            return new Promise(resolve => {
                console.log(`【 ${this.name} 】:第2次睡觉`);
                setTimeout(() => {
                    console.log(`【 ${this.name} 】:第2次睡醒`);
                    resolve();
                }, time * 1000);
            });
        });
        return this;
    }
    
    firstSleep(time) {
        this.taskQueue.unshift(() => {
            return new Promise(resolve => {
                console.log(`【 ${this.name} 】:第1次睡觉`);
                setTimeout(() => {
                    console.log(`【 ${this.name} 】:第1次睡醒了`);
                    resolve();
                }, time * 1000);
            });
        });
        return this;
    }
    
    eat(food) {
        this.taskQueue.push(() => {
            console.log(`【 ${this.name} 】:正在吃${food}`);
            return Promise.resolve();
        });
        return this;
    }
}

5.2 支持更多方法

class LazyMan {
    // ... 前面的代码 ...
    
    work(job) {
        this.taskQueue.push(() => {
            console.log(`【 ${this.name} 】:正在${job}`);
            return Promise.resolve();
        });
        return this;
    }
    
    sleepFirst(time) {
        // 同 firstSleep
        return this.firstSleep(time);
    }
    
    // 支持链式调用完成后的回调
    then(callback) {
        this.promiseChain = this.promiseChain.then(callback);
        return this;
    }
}

六、考察点分析

6.1 Promise 使用

核心考察

  • Promise 链式调用
  • Promise 的 then 方法返回新的 Promise
  • 使用 Promise 实现任务队列的顺序执行

关键代码

let flag = Promise.resolve();
flag = flag.then(fn1);
flag = flag.then(fn2);
flag = flag.then(fn3);

6.2 任务队列

核心考察

  • 使用数组存储任务队列
  • 任务的入队和出队
  • 队列的执行顺序控制

关键代码

this.fun_stack = [];  // 任务队列
this.fun_stack.push(fn);  // 入队
this.fun_stack.unshift(fn);  // 优先入队

6.3 unshift vs push

核心考察

  • push:在数组尾部添加元素
  • unshift:在数组头部添加元素
  • 使用 unshift 实现 firstSleep 的优先执行

对比

方法 位置 用途
push 尾部 普通任务入队
unshift 头部 优先任务入队

6.4 异步执行时机

核心考察

  • 使用 setTimeout 确保所有同步代码执行完毕
  • 理解 JavaScript 的事件循环机制
  • 确保所有任务入队后再开始执行

关键代码

setTimeout(function() {
    self.fun_stack.forEach(fn => {
        flag = flag.then(fn);
    });
});

七、常见变种题目

7.1 变种 1:支持取消

class LazyMan {
    constructor(name) {
        // ... 前面的代码 ...
        this.cancelled = false;
    }
    
    cancel() {
        this.cancelled = true;
        this.taskQueue = [];
        return this;
    }
    
    // 在执行任务前检查是否取消
    executeTask(task) {
        if (this.cancelled) {
            return Promise.resolve();
        }
        return task();
    }
}

7.2 变种 2:支持优先级

class LazyMan {
    constructor(name) {
        // ... 前面的代码 ...
        this.priorityQueue = [];
    }
    
    highPriority(task) {
        this.priorityQueue.push(task);
        return this;
    }
    
    // 执行时优先执行高优先级任务
    execute() {
        // 先执行高优先级任务
        this.priorityQueue.forEach(task => {
            this.promiseChain = this.promiseChain.then(task);
        });
        // 再执行普通任务
        this.taskQueue.forEach(task => {
            this.promiseChain = this.promiseChain.then(task);
        });
    }
}

7.3 变种 3:支持并发控制

class LazyMan {
    constructor(name, concurrency = 1) {
        // ... 前面的代码 ...
        this.concurrency = concurrency;
        this.running = 0;
    }
    
    async executeTask(task) {
        while (this.running >= this.concurrency) {
            await new Promise(resolve => setTimeout(resolve, 10));
        }
        this.running++;
        try {
            await task();
        } finally {
            this.running--;
        }
    }
}

八、面试要点总结

核心知识点

  1. Promise 链式调用:使用 Promise 链实现任务顺序执行
  2. 任务队列:使用数组存储任务,控制执行顺序
  3. 优先级处理:使用 unshift 实现优先执行
  4. 异步时机:使用 setTimeout 确保任务入队后再执行

常见面试题

Q1: 如何实现链式调用?

答:每个方法返回 this,这样就可以连续调用多个方法。

Q2: 如何确保任务按顺序执行?

答:使用 Promise 链,每个任务返回 Promise,下一个任务通过 then 等待上一个任务完成。

Q3: 如何实现 firstSleep 优先执行?

答:使用 unshiftfirstSleep 任务插入队列头部,这样在执行时会优先执行。

Q4: 为什么使用 setTimeout?

答:确保所有同步代码执行完毕,所有任务都已入队后再开始执行。如果不使用 setTimeout,任务队列可能还没有完全构建好就开始执行了。

实战建议

  • ✅ 理解 Promise 链的执行机制
  • ✅ 掌握任务队列的实现方式
  • ✅ 注意异步执行的时机
  • ✅ 理解 unshiftpush 的区别
  • ✅ 考虑边界情况和错误处理

扩展思考

  1. 如何支持任务取消?
  2. 如何支持任务优先级?
  3. 如何支持并发控制?
  4. 如何支持任务重试?
  5. 如何支持任务超时?

这些扩展功能在实际项目中都有应用场景,可以进一步思考和实现。

#前端面试##前端#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

用微笑面对困难:你出于礼貌叫了人一声大姐,大姐很欣慰,她真把你当老弟
点赞 评论 收藏
分享
评论
1
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务