【前端面试小册】JS-第16节:实现 Lazy 链式调用(百度二面)
一、题目描述
1.1 需求分析
实现一个 lazyMan 函数,要求:
- 能够链式调用:支持连续调用多个方法
firstSleep优先执行:无论firstSleep在什么位置都最先调用- 顺序执行:当
firstSleep执行完毕后,后面按照链式调用的顺序执行 - 延迟阻塞:遇到
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 核心思想
- 任务队列:使用数组
fun_stack存储所有待执行的任务 - Promise 链:使用 Promise 链确保任务按顺序执行
- 优先级处理:
firstSleep使用unshift插入队列头部,其他使用push插入队列尾部 - 异步执行:使用
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--;
}
}
}
八、面试要点总结
核心知识点
- Promise 链式调用:使用 Promise 链实现任务顺序执行
- 任务队列:使用数组存储任务,控制执行顺序
- 优先级处理:使用
unshift实现优先执行 - 异步时机:使用
setTimeout确保任务入队后再执行
常见面试题
Q1: 如何实现链式调用?
答:每个方法返回 this,这样就可以连续调用多个方法。
Q2: 如何确保任务按顺序执行?
答:使用 Promise 链,每个任务返回 Promise,下一个任务通过 then 等待上一个任务完成。
Q3: 如何实现 firstSleep 优先执行?
答:使用 unshift 将 firstSleep 任务插入队列头部,这样在执行时会优先执行。
Q4: 为什么使用 setTimeout?
答:确保所有同步代码执行完毕,所有任务都已入队后再开始执行。如果不使用 setTimeout,任务队列可能还没有完全构建好就开始执行了。
实战建议
- ✅ 理解 Promise 链的执行机制
- ✅ 掌握任务队列的实现方式
- ✅ 注意异步执行的时机
- ✅ 理解
unshift和push的区别 - ✅ 考虑边界情况和错误处理
扩展思考
- 如何支持任务取消?
- 如何支持任务优先级?
- 如何支持并发控制?
- 如何支持任务重试?
- 如何支持任务超时?
这些扩展功能在实际项目中都有应用场景,可以进一步思考和实现。
#前端面试##前端#前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

查看6道真题和解析