综合前端面试题(企业级深度)
一、ES6+ 高级语法与高效开发
1. ES6 箭头函数与普通函数的区别
面试题:请详细比较 ES6 箭头函数与普通函数的区别,并说明在哪些场景下不能使用箭头函数。
核心考点:
this指向机制- 构造函数能力
- arguments 对象
- 原型链结构
- 函数名绑定
常见坑点与解决方案:
错误使用箭头函数作为构造函数
- 坑点:箭头函数没有
prototype属性,使用new关键字会报错 - 示例:javascript
const Person = (name) => { this.name = name; }; // TypeError: Person is not a constructor const person = new Person('Alice'); - 解决方案:使用普通函数作为构造函数javascript
function Person(name) { this.name = name; } const person = new Person('Alice'); // 正确
- 坑点:箭头函数没有
在事件回调中使用箭头函数
- 坑点:
this指向定义时的作用域(如 window),而非事件目标元素 - 示例:javascript
button.addEventListener('click', () => { console.log(this); // 指向 window,而非 button 元素 this.innerHTML = 'Clicked'; // 无法修改按钮内容 }); - 解决方案:使用普通函数或通过 event.target 获取目标元素javascript
button.addEventListener('click', function() { console.log(this); // 指向 button 元素 this.innerHTML = 'Clicked'; // 正确修改按钮内容 }); // 或使用箭头函数 + event.target button.addEventListener('click', (event) => { event.target.innerHTML = 'Clicked'; // 通过 event.target 获取目标 });
- 坑点:
在对象方法中使用箭头函数
- 坑点:
this不指向对象实例,而是指向定义时的外部作用域 - 示例:javascript
const counter = { value: 0, increment: () => { this.value++; // this 指向 window,value 始终为 NaN } }; counter.increment(); console.log(counter.value); // NaN - 解决方案:使用普通函数或 ES6 方法简写javascript
const counter = { value: 0, increment() { this.value++; // this 指向 counter 对象 } }; counter.increment(); console.log(counter.value); // 1
- 坑点:
需要动态改变
this的场景- 坑点:
call/apply/bind无法改变箭头函数的this指向 - 示例:javascript
const obj1 = { name: 'obj1' }; const obj2 = { name: 'obj2' }; const getThis = () => this; console.log(getThis.call(obj1)); // 指向 window,而非 obj1 - 解决方案:使用普通函数javascript
function getThis() { return this; } console.log(getThis.call(obj1)); // 指向 obj1
- 坑点:
易错点与解决方案:
误认为箭头函数内部有
arguments对象- 错误:
const fn = () => console.log(arguments);// 引用外部作用域的 arguments - 正确:使用剩余参数
...argsjavascriptconst fn = (...args) => console.log(args); fn(1, 2, 3); // [1, 2, 3]
- 错误:
忽略箭头函数没有
super和new.target- 错误:在箭头函数中使用
super或new.target - 正确:需要这些特性时使用普通函数
- 错误:在箭头函数中使用
对箭头函数的简写语法理解错误
- 错误:
const getObj = () => { name: 'Alice' };// 返回 undefined(被当作代码块) - 正确:使用括号包裹对象字面量javascript
const getObj = () => ({ name: 'Alice' }); // 返回 { name: 'Alice' }
- 错误:
联合记忆:
- 箭头函数 = 没有
this/arguments/new/prototype的简化函数 - 普通函数 = 完整的函数特性(
this动态绑定、构造能力、原型链)
记忆口诀: "箭头函数四没有,this指向定义处;构造事件莫乱用,arguments也没有。"
企业级应用:
- 推荐场景:数组方法回调、简化的函数表达式、需要捕获外部
this的回调 - 禁止场景:构造函数、事件处理函数、对象方法、需要动态
this的函数
// 正确使用:数组方法回调
const users = [1, 2, 3].map(id => getUserById(id));
// 正确使用:捕获外部 this
class User {
constructor(name) {
this.name = name;
// 箭头函数正确捕获实例 this
this.sayHello = () => console.log(`Hello, ${this.name}`);
}
}
const user = new User('Bob');
setTimeout(user.sayHello, 1000); // 正确输出 "Hello, Bob"2. Promise 与异步编程
面试题:请实现一个 Promise 并行限制函数 promiseLimit(tasks, limit),并说明 Promise 的状态变化机制。
核心考点:
- Promise 三种状态(pending/fulfilled/rejected)
- Promise 链式调用与错误捕获
- Promise.all/Promise.race/Promise.allSettled 的区别
- 异步流程控制
常见坑点与解决方案:
Promise 状态不可逆
- 坑点:一旦 Promise 从 pending 状态变为 resolved 或 rejected,状态就不能再改变
- 示例:javascript
const promise = new Promise((resolve, reject) => { resolve('success'); reject('error'); // 无效,状态已变为 resolved }); promise.then(console.log).catch(console.error); // 输出 "success" - 解决方案:确保在 Promise 构造函数中只有一条状态变更路径
忘记处理 Promise 错误
- 坑点:未处理的 Promise 错误会导致应用崩溃或产生未捕获错误警告
- 示例:javascript
const promise = new Promise((resolve, reject) => { reject(new Error('Network error')); }); promise.then(console.log); // 未处理错误,控制台会显示 Uncaught (in promise) Error - 解决方案:始终使用
.catch()或try/catch(对于 async/await)处理错误javascriptpromise.then(console.log).catch(console.error); // 正确处理错误
错误的并行限制实现
- 坑点:未正确管理并发数和任务队列,导致超出限制或任务执行顺序错误
- 示例:直接使用 Promise.all 处理大量任务会导致资源耗尽javascript
// 错误:大量任务同时执行 Promise.all(tasks.map(task => task())).then(console.log); - 解决方案:使用计数器和任务队列实现并行限制(见下文企业级实现)
Promise 回调中的 this 指向
- 坑点:普通函数作为 Promise 回调时,this 指向可能不符合预期
- 示例:javascript
const obj = { value: 1, async getValue() { // this 指向 obj return new Promise((resolve) => { setTimeout(function() { resolve(this.value); // this 指向 window,返回 undefined }, 100); }); } }; - 解决方案:使用箭头函数或绑定 thisjavascript
setTimeout(() => resolve(this.value), 100); // 箭头函数继承外部 this // 或 setTimeout(function() { resolve(this.value); }.bind(this), 100);
易错点与解决方案:
混淆 Promise.all 与 Promise.race 的行为
- 错误:
Promise.all([p1, p2])只要有一个失败就拒绝,Promise.race([p1, p2])返回第一个完成的结果 - 正确:javascript
// Promise.all:全部成功才成功,任一失败则失败 Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(console.log); // [1, 2] Promise.all([Promise.resolve(1), Promise.reject(2)]).catch(console.error); // 2 // Promise.race:返回第一个完成(成功或失败)的结果 Promise.race([Promise.resolve(1), Promise.reject(2)]).then(console.log); // 1 Promise.race([Promise.reject(1), Promise.resolve(2)]).catch(console.error); // 1
- 错误:
忽略 Promise.then 中的错误会冒泡
- 错误:认为每个 then 都需要单独处理错误
- 正确:错误会沿链式调用冒泡,只需在最后添加一个 catchjavascript
fetch('url1') .then(res => res.json()) .then(data => fetch('url2', { body: data })) .then(res => res.json()) .catch(error => console.error('Any error:', error)); // 捕获所有错误
错误处理顺序不当导致的错误丢失
- 错误:在 then 之后立即添加 catch,导致后续 then 无法获取正确结果
- 正确:将 catch 放在链式调用的末尾javascript
// 错误:catch 之后的 then 会继续执行,即使前面有错误 fetch('url') .catch(error => console.error(error)) .then(data => console.log(data)); // 可能处理 undefined // 正确:catch 捕获所有错误,后续 then 只有在成功时才执行 fetch('url') .then(data => console.log(data)) .catch(error => console.error(error));
联合记忆:
- Promise 状态:pending → resolved/rejected(不可逆)
- 错误处理:链式调用中的
.catch()可以捕获前面所有 Promise 的错误 - 并行控制:使用计数器和任务队列实现限制
记忆口诀: "Promise 有三态,pending变settled;all等全成功,race看谁快;错误要捕获,不然冒泡外。"
企业级实现 - Promise 并行限制
核心逻辑说明:
- 初始化:创建结果数组、索引、运行计数器和任务队列
- 执行函数:定义
executeNext函数处理任务执行逻辑 - 任务调度:当运行任务数小于限制且有未执行任务时,启动新任务
- 状态管理:任务完成后更新运行计数器,并递归调用
executeNext - 结果返回:当所有任务完成时,resolve 结果数组
function promiseLimit(tasks, limit) {
const results = [];
let index = 0; // 当前要执行的任务索引
let running = 0; // 正在运行的任务数
return new Promise((resolve, reject) => {
// 执行下一个任务
const executeNext = () => {
// 所有任务执行完成,resolve 结果
if (index >= tasks.length && running === 0) {
resolve(results);
return;
}
// 启动尽可能多的任务,直到达到限制
while (running < limit && index < tasks.length) {
const taskIndex = index++; // 保存当前任务索引
running++;
tasks[taskIndex]()
.then(result => {
results[taskIndex] = result; // 按原顺序保存结果
})
.catch(error => {
results[taskIndex] = error; // 错误也按原顺序保存
})
.finally(() => {
running--; // 任务完成,运行计数器减1
executeNext(); // 递归执行下一个任务
});
}
};
executeNext(); // 启动执行
});
}3. async/await 异步编程
面试题:async/await 相比 Promise 有哪些优势?如何处理 async/await 中的错误?请举例说明。
核心考点:
- async/await 语法糖本质
- 错误处理机制
- 并发控制
- 与 Promise 的关系
常见坑点:
忘记使用 await:导致异步操作未等待完成
- 解决方案:在异步操作前添加 await 关键字javascript
// 错误:未等待异步操作完成 async function getData() { fetch('/api/data'); // 没有 await } // 正确:等待异步操作完成 async function getData() { await fetch('/api/data'); }
- 解决方案:在异步操作前添加 await 关键字
错误的并发处理:使用串行 await 而非 Promise.all 导致性能问题
- 解决方案:使用 Promise.all 并行处理异步操作javascript
// 错误:串行执行(耗时 = 3秒 + 2秒 + 1秒 = 6秒) async function fetchData() { const data1 = await fetch('/api/data1'); // 3秒 const data2 = await fetch('/api/data2'); // 2秒 const data3 = await fetch('/api/data3'); // 1秒 return [data1, data2, data3]; } // 正确:并行执行(耗时 = max(3秒, 2秒, 1秒) = 3秒) async function fetchData() { const [data1, data2, data3] = await Promise.all([ fetch('/api/data1'), fetch('/api/data2'), fetch('/api/data3') ]); return [data1, data2, data3]; }
- 解决方案:使用 Promise.all 并行处理异步操作
未处理的 async 函数错误:async 函数返回的 Promise 错误未被捕获
- 解决方案:使用 try/catch 或 .catch() 处理错误javascript
// 方法1:try/catch async function fetchData() { try { const response = await fetch('/api/data'); return await response.json(); } catch (error) { console.error('请求失败:', error); return null; } } // 方法2:.catch() fetchData().catch(error => { console.error('处理失败:', error); });
- 解决方案:使用 try/catch 或 .catch() 处理错误
在循环中错误使用 await:导致串行执行而非并行
- 解决方案:使用 Promise.all + map 实现并行处理javascript
// 错误:串行执行 async function processItems(items) { for (const item of items) { await processItem(item); } } // 正确:并行执行 async function processItems(items) { await Promise.all(items.map(item => processItem(item))); }
- 解决方案:使用 Promise.all + map 实现并行处理
易错点:
误认为 async/await 是新的异步机制(本质是 Promise + Generator)
- 解决方案:理解 async/await 的本质javascript
// async/await 只是语法糖 async function fetchData() { return await fetch('/api/data'); } // 等价于 Promise function fetchData() { return fetch('/api/data'); }
- 解决方案:理解 async/await 的本质
忽略 try/catch 可以捕获 await 操作的错误
- 解决方案:在 try/catch 中包裹 await 操作javascript
async function fetchData() { try { const response = await fetch('/api/invalid-url'); return await response.json(); } catch (error) { console.error('捕获到错误:', error); } }
- 解决方案:在 try/catch 中包裹 await 操作
错误地使用 await 在非 Promise 对象上
- 解决方案:确保 await 后面是 Promise 对象javascript
// 非 Promise 对象会被包装成已解决的 Promise async function test() { const result = await 123; console.log(result); // 123 const obj = await { name: 'test' }; console.log(obj); // { name: 'test' } }
- 解决方案:确保 await 后面是 Promise 对象
联合记忆:
- async 函数 = Promise 包装器
- await = Promise.then 的语法糖
- 错误处理 = try/catch 替代 Promise.catch
记忆口诀: "async 函数返回 Promise,await 等待 Promise 成;错误处理用 try/catch,并发要用 Promise.all。"
企业级案例:
// 错误的串行执行
async function fetchAllData() {
const user = await fetchUser();
const posts = await fetchPosts(); // 必须等 user 完成
return { user, posts };
}
// 正确的并行执行
async function fetchAllData() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
return { user, posts };
}
// 错误处理
async function safeFetch() {
try {
const data = await fetchData();
return data;
} catch (error) {
console.error('Fetch failed:', error);
return null;
}
}二、Vue 全家桶与源码解析
1. Vue 2 响应式原理
面试题:请详细描述 Vue 2 的响应式原理,包括 Object.defineProperty 的缺陷以及如何解决。
核心考点:
- Object.defineProperty 实现
- 依赖收集机制
- 发布-订阅模式
- 响应式系统的缺陷
常见坑点与解决方案:
新增属性无法响应
- 坑点:Object.defineProperty 只能监听对象创建时已存在的属性,新增属性不会触发视图更新
- 示例:javascript
const vm = new Vue({ data() { return { user: { name: 'Alice' } }; } }); // 新增属性,视图不会更新 vm.user.age = 20; - 解决方案:使用
Vue.set()或this.$set()新增响应式属性javascriptthis.$set(vm.user, 'age', 20); // 视图会更新 // 或使用 Object.assign 创建新对象 vm.user = Object.assign({}, vm.user, { age: 20 });
数组索引和长度变化无法响应
- 坑点:直接修改数组索引或长度不会触发响应式更新
- 示例:javascript
const vm = new Vue({ data() { return { list: [1, 2, 3] }; } }); // 直接修改索引,视图不会更新 vm.list[0] = 10; // 修改长度,视图不会更新 vm.list.length = 2; - 解决方案:使用 Vue 重写的数组方法或
Vue.set()javascript// 使用重写的数组方法 vm.list.splice(0, 1, 10); // 视图会更新 // 使用 Vue.set() this.$set(vm.list, 0, 10); // 视图会更新
深度嵌套对象的性能问题
- 坑点:Vue 会递归遍历对象的所有嵌套属性,大型嵌套对象会导致性能问题
- 示例:javascript
const vm = new Vue({ data() { return { // 大型嵌套对象,初始化性能差 deepData: { /* 嵌套层级很深的对象 */ } }; } }); - 解决方案:
- 避免不必要的深度嵌套
- 使用
Object.freeze()冻结不需要响应的对象 - 考虑使用 Vue 3 的 Proxy(自动实现响应式,性能更好)
响应式对象与原始对象的混淆
- 坑点:将响应式对象的属性赋值给原始对象,修改原始对象不会触发更新
- 示例:javascript
const vm = new Vue({ data() { return { user: { name: 'Alice' } }; } }); // 获取响应式对象的属性(原始值) let name = vm.user.name; // 修改原始值,不会触发更新 name = 'Bob'; - 解决方案:始终操作响应式对象的属性,而不是原始值javascript
vm.user.name = 'Bob'; // 正确,视图会更新
易错点与解决方案:
误认为 Vue.set 可以监听所有数组索引变化
- 错误:
Vue.set(arr, index, value)对数组的支持有限,大型数组索引操作可能不高效 - 正确:优先使用 Vue 重写的数组方法(push, pop, shift, unshift, splice, sort, reverse)
- 错误:
忽略对象冻结会破坏响应式
- 错误:对冻结的对象使用
Vue.set不会生效 - 示例:javascript
const vm = new Vue({ data() { return { user: Object.freeze({ name: 'Alice' }) }; } }); this.$set(vm.user, 'age', 20); // 无效,对象已冻结
- 错误:对冻结的对象使用
对 Vue.$set 的工作原理理解错误
- 错误:认为
Vue.$set是给对象添加新属性 - 正确:
Vue.$set是通过重新定义属性的 getter/setter 使其响应式
- 错误:认为
联合记忆:
- 响应式 = Object.defineProperty + 依赖收集 + 发布-订阅
- 缺陷 = 新增属性/数组变化/深度嵌套/性能问题
- 解决方案 = Vue.set/Vue.delete/重写数组方法
记忆口诀: "Vue 响应式靠 defineProperty,get 收集依赖 set 通知;新增属性不响应,数组方法要重写;深度嵌套有性能,Vue.set 来救场。"
源码核心 - Vue 2 响应式实现
核心逻辑说明:
- 递归监听:
observe()函数递归处理嵌套对象,使其所有属性都响应式 - 依赖收集:通过
Dep类管理依赖,在get方法中收集 Watcher - 通知更新:在
set方法中通知所有依赖的 Watcher 更新 - 深度监听:新值设置时重新调用
observe()确保新值也是响应式
// Vue 2 响应式核心实现(简化版)
function defineReactive(obj, key, val) {
// 递归处理嵌套对象,使其响应式
observe(val);
// 创建依赖收集器,管理该属性的所有依赖
const dep = new Dep();
// 使用 Object.defineProperty 定义响应式属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集:将当前 Watcher 添加到依赖列表
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
// 值未变化,直接返回
if (newVal === val) return;
// 更新值
val = newVal;
// 深度监听新值,确保新值也是响应式
observe(newVal);
// 通知更新:触发所有依赖的 Watcher 更新
dep.notify();
}
});
}2. Vue 3 Composition API
面试题:Vue 3 Composition API 相比 Vue 2 Options API 有哪些优势?请结合源码分析其实现原理。
核心考点:
- Composition API 设计思想
- setup 函数生命周期
- 响应式 API 实现
- 逻辑复用机制
常见坑点:
- setup 函数中的 this 指向:setup 中 this 为 undefined
- 响应式数据的错误使用:直接修改 reactive 对象的属性与修改 ref 的 value
- 生命周期钩子的变化:需要使用 onMounted 等函数式钩子
- 逻辑复用的过度使用:导致代码难以追踪
易错点:
- 混淆 ref 与 reactive 的使用场景
- 忽略 computed 的缓存机制
- 错误地在 setup 中访问 DOM(需使用 ref + onMounted)
联合记忆:
- Composition API = 逻辑复用 + 类型友好 + 灵活组织
- 响应式 API = ref(基本类型) + reactive(引用类型) + computed(计算属性)
- 生命周期 = onMounted/onUpdated/onUnmounted 等
记忆口诀: "Composition API 好,逻辑复用真巧妙;setup 函数无 this,响应式用 ref/reactive;生命周期要前缀,逻辑组合更灵活。"
源码核心:
// Vue 3 ref 实现原理(简化版)
function ref(value) {
const refObject = {
_isRef: true,
get value() {
// 依赖收集
track(refObject, 'value');
return value;
},
set value(newValue) {
value = newValue;
// 通知更新
trigger(refObject, 'value');
}
};
return refObject;
}
// Vue 3 reactive 实现原理(简化版)
function reactive(target) {
return createReactiveObject(target, false, mutableHandlers);
}
// 代理处理器
const mutableHandlers = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 递归处理嵌套对象
if (isObject(result)) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
};3. Vue Router 原理
面试题:Vue Router 的两种模式(hash 和 history)有什么区别?如何实现一个简单的 Vue Router?
核心考点:
- hash 与 history 模式的实现原理
- 路由守卫机制
- 路由懒加载
- 动态路由与嵌套路由
常见坑点:
history 模式的 404 问题:需要后端配置 fallback
- 解决方案:nginx
# Nginx 配置 location / { try_files $uri $uri/ /index.html; }- 原理:将所有请求转发到 index.html,由前端路由处理
- 解决方案:
路由懒加载的错误写法:未正确使用 import() 函数
- 错误写法:
component: require('../components/Home.vue') - 正确写法:
component: () => import('../components/Home.vue')
- 错误写法:
路由守卫的执行顺序:全局守卫、路由守卫、组件守卫的顺序
- 解决方案:明确守卫执行顺序(全局 beforeEach → 路由 beforeEnter → 组件 beforeRouteEnter → 全局 beforeResolve → 组件渲染 → 全局 afterEach)
动态路由的权限控制:需要结合路由守卫实现
- 解决方案:javascript
// 全局前置守卫 router.beforeEach((to, from, next) => { const userRole = store.state.user.role; if (to.meta.requiredRoles && !to.meta.requiredRoles.includes(userRole)) { next('/403'); } else { next(); } });
- 解决方案:
易错点:
混淆 router.push 与 router.replace 的区别
- 解决方案:明确用途 - push 添加历史记录,replace 替换当前记录
- 代码示例:javascript
// 添加新历史记录 router.push('/home'); // 替换当前历史记录 router.replace('/login');
忽略路由参数变化不会触发组件重新渲染
- 解决方案:使用 watch 监听路由参数变化或 beforeRouteUpdate 守卫
- 代码示例:javascript
// 方法1:watch 监听 watch: { $route(to, from) { this.loadData(to.params.id); } } // 方法2:beforeRouteUpdate 守卫 beforeRouteUpdate(to, from, next) { this.loadData(to.params.id); next(); }
对路由元信息(meta)的错误使用
- 解决方案:在路由配置中定义 meta 信息,在守卫中使用
- 代码示例:javascript
// 路由配置 const routes = [ { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true, title: '仪表盘' } } ]; // 在守卫中使用 router.beforeEach((to, from, next) => { if (to.meta.title) { document.title = to.meta.title; } next(); });
联合记忆:
- hash 模式 = URL 锚点 + onhashchange 事件
- history 模式 = HTML5 History API + 后端配置
- 路由守卫 = 全局(beforeEach/beforeResolve/afterEach) + 路由(beforeEnter) + 组件(beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave)
记忆口诀: "Vue Router 两种模式,hash 简单 history 美;hash 靠锚点,history 要后端;路由守卫分三层,全局路由组件齐。"
简易实现:
// 简单的 Vue Router 实现
class VueRouter {
constructor(options) {
this.options = options;
this.routeMap = {};
this.app = new Vue({ data: { currentRoute: '/' } });
this.init();
this.createRouteMap();
this.initComponents(Vue);
}
init() {
// 监听 URL 变化
this.hashChange = this.hashChange.bind(this);
window.addEventListener('load', this.hashChange);
window.addEventListener('hashchange', this.hashChange);
}
createRouteMap() {
// 构建路由映射
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component;
});
}
initComponents(Vue) {
// 注册 router-link 和 router-view 组件
Vue.component('router-link', {
props: { to: String },
render(h) {
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default);
}
});
Vue.component('router-view', {
render: h => {
const component = this.routeMap[this.app.currentRoute];
return h(component);
}
});
}
hashChange() {
this.app.currentRoute = window.location.hash.slice(1) || '/';
}
}4. Vuex/Pinia 状态管理
面试题:Vuex 与 Pinia 有哪些区别?请结合企业级应用场景说明如何选择。
核心考点:
- 状态管理的核心概念
- Vuex 与 Pinia 的架构差异
- TypeScript 支持
- 模块化设计
- 持久化存储
常见坑点:
- Vuex 模块化的命名空间问题:忘记启用 namespaced
- 状态更新的方式错误:直接修改 state 而非通过 mutation
- 异步操作的处理:在 mutation 中执行异步操作
- Pinia 的自动导入配置错误:导致 store 无法访问
易错点:
- 混淆 Vuex 的 mutation 与 action
- 忽略 Pinia 可以直接修改 state
- 对状态持久化的实现方式理解错误
联合记忆:
- Vuex = 集中式存储 + 严格的单向数据流 + 模块化
- Pinia = 模块化设计 + TypeScript 友好 + 灵活的 API + 轻量级
- 选择依据 = 项目规模 + TypeScript 支持 + 团队熟悉度
记忆口诀: "Vuex 老大哥,单向数据流;mutation 同步,action 异步;模块要命名空间,使用稍显复杂;Pinia 新贵来,API 更简洁;支持 TypeScript,状态直接改;模块化更灵活,轻量又好用。"
企业级选择:
- 小型项目:直接使用 Composition API 的响应式 API
- 中型项目:Pinia(更好的 TypeScript 支持和开发体验)
- 大型项目:Vuex(更严格的数据流控制,适合复杂业务)
三、构建工具(Webpack/Rollup)
1. Webpack 核心原理
面试题:请详细描述 Webpack 的工作原理,包括核心概念、构建流程和优化策略。
核心考点:
- Webpack 核心概念(Entry/Output/Loader/Plugin)
- 构建流程(初始化/编译/输出)
- 依赖图构建
- 代码分割策略
- 性能优化
常见坑点:
- Loader 配置顺序错误:Webpack 按从右到左的顺序执行 Loader
- Plugin 配置不当:重复使用或配置错误导致构建失败
- 代码分割的错误使用:导致 chunk 过多或过大
- Tree Shaking 不生效:未使用 ES6 模块或配置错误
易错点:
- 混淆 Loader 与 Plugin 的作用(Loader 处理文件,Plugin 扩展功能)
- 忽略 Webpack 缓存机制的配置
- 错误地使用 resolve.alias 导致模块解析错误
联合记忆:
- Webpack 工作流 = 入口分析 → 依赖图构建 → Loader 转换 → Plugin 处理 → 输出文件
- 优化策略 = 代码分割 + Tree Shaking + 缓存 + 多线程构建
- 核心概念 = Entry/Output/Loader/Plugin/Chunk/Module
记忆口诀: "Webpack 是打包机,核心概念要牢记;Entry 入口 Output 出,Loader 处理文件 Plugin 扩展;构建流程分三步,初始化编译再输出;优化策略多,代码分割 Tree Shaking 不可少。"
核心配置:
// Webpack 核心配置(企业级优化版)
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 缓存 Babel 编译结果
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new webpack.optimize.SplitChunksPlugin({
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
})
],
optimization: {
usedExports: true, // 启用 Tree Shaking
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true // 多线程压缩
})
]
}
};2. Webpack 自定义插件
面试题:如何编写一个 Webpack 自定义插件?请实现一个简单的插件来分析构建时间。
核心考点:
- Webpack 插件 API
- Tapable 事件流
- 插件生命周期
- 构建分析
常见坑点:
错误的钩子时机选择:选择不适合的生命周期钩子
- 解决方案:根据插件功能选择正确的钩子
- 编译开始:
compile/compilation - 文件处理:
emit/afterEmit - 编译结束:
done
- 编译开始:
- 参考:Webpack Hooks 文档
- 解决方案:根据插件功能选择正确的钩子
未正确处理异步操作:异步插件需要调用 callback 或返回 Promise
- 解决方案:
- 异步串行钩子:使用
tapAsync+callback - 异步并行钩子:使用
tapPromise+Promise
- 异步串行钩子:使用
- 代码示例:javascript
// 异步串行钩子 compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { // 异步操作 setTimeout(() => { console.log('异步操作完成'); callback(); }, 1000); }); // 异步并行钩子 compiler.hooks.emit.tapPromise('MyPlugin', (compilation) => { // 异步操作 return new Promise(resolve => { setTimeout(() => { console.log('异步操作完成'); resolve(); }, 1000); }); });
- 解决方案:
插件实例化错误:忘记使用 new 关键字创建插件实例
- 解决方案:使用 new 关键字实例化插件
- 错误:
plugins: [MyPlugin] - 正确:
plugins: [new MyPlugin()]
- 错误:
- 解决方案:使用 new 关键字实例化插件
内存泄漏:未正确清理事件监听器
- 解决方案:在插件中添加清理逻辑javascript
apply(compiler) { const handleEmit = (compilation) => { // 处理逻辑 }; compiler.hooks.emit.tap('MyPlugin', handleEmit); // 在插件卸载时清理 compiler.hooks.done.tap('MyPlugin', () => { compiler.hooks.emit.tap.remove('MyPlugin', handleEmit); }); }
- 解决方案:在插件中添加清理逻辑
易错点:
混淆同步钩子与异步钩子的使用
- 解决方案:根据钩子类型选择正确的注册方法
- 同步钩子:
tap - 异步串行:
tapAsync - 异步并行:
tapPromise
- 同步钩子:
- 解决方案:根据钩子类型选择正确的注册方法
忽略 Webpack 版本的 API 差异
- 解决方案:
- 查看官方文档,注意版本兼容性
- 使用 webpack-merge 处理不同版本配置
javascript// 使用 webpack-merge 处理版本差异 const { merge } = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); module.exports = merge(commonConfig, { // 特定版本的配置 });
- 解决方案:
对 Tapable 事件流的工作原理理解错误
- 解决方案:
- 学习 Tapable 库的基本使用
- 理解不同钩子类型的执行机制
javascriptconst { SyncHook, AsyncParallelHook } = require('tapable'); // 创建同步钩子 const syncHook = new SyncHook(['arg1', 'arg2']); syncHook.tap('Hook1', (arg1, arg2) => { console.log('Hook1:', arg1, arg2); });
- 解决方案:
联合记忆:
- Webpack 插件 = 构造函数 + apply 方法 + 钩子注册
- 钩子类型 = 同步(SyncHook)/异步并行(AsyncParallelHook)/异步串行(AsyncSeriesHook)
- 实现步骤 = 注册钩子 → 处理逻辑 → 调用回调
记忆口诀: "Webpack 插件构造好,apply 方法不能少;Tapable 事件流,钩子时机要选好;同步异步要分清,异步别忘 callback;插件功能强,扩展 Webpack 没商量。"
企业级实现:
// Webpack 构建时间分析插件
class BuildTimePlugin {
constructor(options = {}) {
this.options = options;
this.startTime = 0;
this.endTime = 0;
}
apply(compiler) {
// 注册编译开始钩子
compiler.hooks.compile.tap('BuildTimePlugin', () => {
this.startTime = Date.now();
console.log('Build started:', new Date().toLocaleString());
});
// 注册编译完成钩子(异步)
compiler.hooks.done.tapAsync('BuildTimePlugin', (stats, callback) => {
this.endTime = Date.now();
const buildTime = this.endTime - this.startTime;
console.log('Build completed:', new Date().toLocaleString());
console.log('Total build time:', buildTime, 'ms');
// 可以将构建时间写入文件或发送到监控系统
if (this.options.outputFile) {
fs.writeFileSync(
this.options.outputFile,
JSON.stringify({ buildTime, timestamp: this.endTime })
);
}
callback();
});
}
}
module.exports = BuildTimePlugin;3. Rollup 核心原理
面试题:Rollup 与 Webpack 有哪些区别?请结合使用场景说明如何选择。
核心考点:
- Rollup 与 Webpack 的设计理念差异
- Tree Shaking 实现
- 输出格式支持
- 使用场景选择
- 性能对比
常见坑点:
Rollup 处理 CommonJS 模块:需要 @rollup/plugin-commonjs 插件
- 解决方案:安装并配置 @rollup/plugin-commonjs 和 @rollup/plugin-node-resolvejavascript
import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; export default { // ... plugins: [ resolve(), // 解析 Node.js 模块 commonjs() // 将 CommonJS 转为 ES6 ] };
- 解决方案:安装并配置 @rollup/plugin-commonjs 和 @rollup/plugin-node-resolve
动态导入的处理:Rollup 对动态导入的支持与 Webpack 不同
- 解决方案:使用 output.dir 替代 output.file 并启用 experimentalTopLevelAwaitjavascript
export default { input: 'src/index.js', output: { dir: 'dist', // 使用 dir 而非 file format: 'es' }, experimentalTopLevelAwait: true };
- 解决方案:使用 output.dir 替代 output.file 并启用 experimentalTopLevelAwait
代码分割的配置:Rollup 的代码分割配置与 Webpack 差异较大
- 解决方案:使用 output.manualChunks 自定义代码分割javascript
export default { // ... output: { dir: 'dist', format: 'es', manualChunks: { // 将 lodash 单独打包 'lodash': ['lodash'], // 将 react 相关模块单独打包 'react': ['react', 'react-dom'] } } };
- 解决方案:使用 output.manualChunks 自定义代码分割
开发服务器的缺乏:需要额外配置(如 rollup-plugin-serve)
- 解决方案:安装并配置 rollup-plugin-serve 和 rollup-plugin-livereloadjavascript
import serve from 'rollup-plugin-serve'; import livereload from 'rollup-plugin-livereload'; export default { // ... plugins: [ // ... serve({ open: true, port: 3000, contentBase: 'dist' }), livereload('dist') ] };
- 解决方案:安装并配置 rollup-plugin-serve 和 rollup-plugin-livereload
易错点:
误认为 Rollup 性能一定比 Webpack 好(取决于使用场景)
- 解决方案:根据项目类型选择合适的打包工具
- 库打包:Rollup(更小的体积,更好的 Tree Shaking)
- 应用打包:Webpack(更好的动态导入支持,丰富的插件生态)
- 解决方案:根据项目类型选择合适的打包工具
忽略 Rollup 对浏览器环境的兼容性处理
- 解决方案:使用 @rollup/plugin-babel 和 @babel/preset-envjavascript
import babel from '@rollup/plugin-babel'; export default { // ... plugins: [ // ... babel({ exclude: 'node_modules/**', presets: [ ['@babel/preset-env', { targets: { browsers: ['> 1%, last 2 versions'] } }] ] }) ] };
- 解决方案:使用 @rollup/plugin-babel 和 @babel/preset-env
对 Rollup 的输出格式(es/cjs/amd/iife/umd)理解错误
解决方案:明确各种输出格式的使用场景
格式 用途 示例 es ES模块(现代浏览器) import { foo } from './lib'cjs CommonJS模块(Node.js) const { foo } = require('./lib')amd AMD模块(RequireJS) define(['lib'], function(lib) {})iife 自执行函数(浏览器) <script src="lib.js"></script>umd 通用模块(浏览器/Node.js/AMD) 支持多种导入方式 javascript// 输出多种格式 export default { // ... output: [ { file: 'dist/lib.es.js', format: 'es' }, { file: 'dist/lib.cjs.js', format: 'cjs' }, { file: 'dist/lib.umd.js', format: 'umd', name: 'MyLibrary' } ] };
联合记忆:
- Rollup = 下一代 ES 模块打包器 + 优秀的 Tree Shaking + 适合库打包
- Webpack = 功能强大的模块打包器 + 丰富的生态 + 适合应用打包
- 选择依据 = 项目类型(库/应用) + 功能需求 + 性能要求
记忆口诀: "Rollup 轻量又快速,Tree Shaking 真厉害;适合打包库,输出格式多;Webpack 功能全,生态丰富应用广;库用 Rollup,应用用 Webpack。"
核心配置:
// Rollup 核心配置(库打包版)
export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.cjs.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/bundle.esm.js',
format: 'es',
sourcemap: true
},
{
file: 'dist/bundle.umd.js',
format: 'umd',
name: 'MyLibrary',
sourcemap: true
}
],
plugins: [
resolve(),
commonjs(),
babel({
exclude: 'node_modules/**'
}),
terser()
],
external: ['react', 'react-dom'] // 外部依赖
};四、Node.js 框架(Koa/Express)
1. Koa 核心原理
面试题:Koa 与 Express 有哪些区别?请详细描述 Koa 的中间件机制。
核心考点:
- 中间件机制(洋葱模型)
- 异步流程处理
- 上下文(Context)对象
- 错误处理机制
常见坑点:
- 中间件执行顺序错误:不理解洋葱模型的执行流程
- 异步中间件的错误处理:未正确使用 try/catch 或 await
- 上下文对象的生命周期:每个请求创建一个新的 Context
- 响应处理的时机:需要确保在中间件链中调用 response.end()
易错点:
- 混淆 Koa 的中间件与 Express 的中间件
- 忽略 Koa 2 使用 async/await 处理异步
- 对 Koa 的错误处理机制理解错误
联合记忆:
- Koa = 轻量级框架 + 洋葱模型中间件 + 异步友好
- Express = 功能丰富 + 回调函数 + 内置路由/模板引擎
- 中间件 = 洋葱模型(先进后出) + 异步处理
记忆口诀: "Koa 框架轻量级,中间件用洋葱模型;async/await 处理异步,上下文对象真方便;Express 功能全,回调函数处理异步;Koa 2 异步好,Express 传统稳。"
核心实现:
// Koa 中间件洋葱模型实现(简化版)
class Koa {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
return this;
}
async callback(ctx) {
const dispatch = (i) => {
if (i >= this.middlewares.length) return Promise.resolve();
const middleware = this.middlewares[i];
// 递归调用下一个中间件
return Promise.resolve(middleware(ctx, () => dispatch(i + 1)));
};
return dispatch(0);
}
listen(...args) {
const server = http.createServer(async (req, res) => {
const ctx = this.createContext(req, res);
await this.callback(ctx);
this.response(ctx);
});
return server.listen(...args);
}
}2. Express 路由与中间件
面试题:如何在 Express 中实现路由模块化?请实现一个简单的认证中间件。
核心考点:
- 路由模块化
- 中间件分类(应用级/路由级/错误处理/内置)
- 认证机制
- 错误处理
常见坑点:
- 中间件顺序错误:路由中间件应放在通用中间件之后
- 错误处理中间件位置:必须放在所有路由中间件之后
- 路由路径匹配错误:Express 的路由匹配规则(先到先得)
- 异步中间件的错误处理:需要显式调用 next(err)
易错点:
- 混淆 app.use 与 app.get/app.post 的区别
- 忽略中间件的 next() 调用
- 对路由参数的处理错误
联合记忆:
- Express 中间件 = 函数(req, res, next)
- 路由模块化 = express.Router()
- 错误处理 = 四参数中间件(err, req, res, next)
记忆口诀: "Express 中间件,req res next 传;应用路由错误级,顺序摆放很关键;路由模块化用 Router,错误处理在最后;异步错误要 next(err),否则服务器崩溃。"
企业级实现:
// Express 路由模块化
const express = require('express');
const router = express.Router();
// 认证中间件
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
// 受保护的路由
router.get('/profile', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
module.exports = router;
// 应用入口
const app = express();
app.use('/api', require('./routes/api'));
// 错误处理中间件(必须放在最后)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something broke!' });
});五、CSS 预处理器与后处理器
1. Sass/Less 预处理器
面试题:Sass 与 Less 有哪些区别?请实现一个响应式布局的 Mixin。
核心考点:
- 变量、嵌套、Mixin、继承
- 编译方式(Sass 有缩进式语法)
- 响应式布局实现
- 性能对比
常见坑点:
- 过度嵌套导致的 CSS 权重问题:嵌套层级过深会导致选择器权重过高
- Mixin 的性能问题:每个 Mixin 调用都会生成重复的 CSS
- 变量作用域的理解错误:Sass/Less 的变量作用域规则不同
- 编译错误的调试:预处理器的错误信息可能不够友好
易错点:
- 混淆 Sass 的缩进式语法与 SCSS 语法
- 忽略 Less 的变量延迟加载特性
- 对 Mixin 与继承的使用场景理解错误
联合记忆:
- Sass = Ruby 编写 + 缩进式语法 + 功能强大 + 编译速度快
- Less = JavaScript 编写 + 类 CSS 语法 + 易于上手 + 编译速度较慢
- 核心特性 = 变量($/@) + 嵌套 + Mixin + 继承
记忆口诀: "Sass Less 预处理器,变量嵌套 Mixin 齐;Sass 缩进式,Less 类 CSS;Mixin 代码复用,继承样式合并;响应式布局用 Mixin,适配各种屏幕。"
企业级实现:
// Sass 响应式 Mixin
@mixin responsive($breakpoint) {
@if $breakpoint == 'sm' {
@media (min-width: 576px) { @content; }
}
@else if $breakpoint == 'md' {
@media (min-width: 768px) { @content; }
}
@else if $breakpoint == 'lg' {
@media (min-width: 992px) { @content; }
}
@else if $breakpoint == 'xl' {
@media (min-width: 1200px) { @content; }
}
}
// 使用示例
.container {
width: 100%;
padding: 0 15px;
@include responsive(md) {
width: 750px;
margin: 0 auto;
}
@include responsive(lg) {
width: 970px;
}
@include responsive(xl) {
width: 1170px;
}
}2. PostCSS 后处理器
面试题:什么是 PostCSS?请实现一个简单的 PostCSS 插件来添加 CSS 前缀。
核心考点:
- PostCSS 与预处理器的区别
- PostCSS 插件生态
- AST 操作
- 自动前缀添加
常见坑点:
- PostCSS 配置顺序错误:插件执行顺序会影响最终结果
- 过度使用插件导致的性能问题:每个插件都会解析一次 AST
- 与预处理器的配合问题:需要注意执行顺序
- AST 操作的复杂性:修改 CSS AST 需要理解其结构
易错点:
- 混淆 PostCSS 与预处理器
- 忽略 PostCSS 的配置文件(postcss.config.js)
- 对 PostCSS 插件的工作原理理解错误
联合记忆:
- PostCSS = CSS 解析器 + AST 操作 + 插件系统 + 后处理器
- 核心插件 = autoprefixer + postcss-preset-env + cssnano
- 与预处理器的配合 = 预处理器编译 → PostCSS 处理
记忆口诀: "PostCSS 是后处理器,解析 CSS 成 AST;插件系统真强大,自动前缀轻松加;与预处理器配合好,编译顺序很重要;AST 操作要熟练,插件开发不难搞。"
核心实现:
// PostCSS 自动前缀插件(简化版)
module.exports = (opts = {}) => {
return {
postcssPlugin: 'postcss-autoprefix',
Rule (rule) {
// 遍历所有规则
rule.walkDecls(decl => {
// 需要添加前缀的属性
const prefixProps = ['transform', 'transition', 'animation'];
if (prefixProps.includes(decl.prop)) {
// 添加前缀属性
rule.prepend({
prop: `-webkit-${decl.prop}`,
value: decl.value
});
}
});
}
};
};
module.exports.postcss = true;
// PostCSS 配置
module.exports = {
plugins: [
require('autoprefixer')({
overrideBrowserslist: ['> 1%', 'last 2 versions']
}),
require('cssnano')()
]
};六、组件库开发与工程化
1. 通用组件库开发
面试题:如何设计并开发一个通用的组件库?请描述从设计到发布的完整流程。
核心考点:
- 组件设计原则(单一职责、可复用、可扩展)
- 组件库架构(Monorepo)
- 构建与打包策略
- 文档系统
- 发布与版本管理
常见坑点:
- 组件设计过度复杂:违反单一职责原则
- 样式隔离问题:组件样式与用户样式冲突
- 文档系统不完善:缺乏示例和 API 文档
- 版本管理混乱:未遵循语义化版本规范
易错点:
- 忽略组件的可访问性(A11y)
- 对组件的 API 设计考虑不周
- 未处理组件的边界情况
- 忽略构建产物的体积优化
联合记忆:
- 组件设计 = 单一职责 + 可复用 + 可扩展 + 可访问
- 工程化 = Monorepo + Rollup/Vite 打包 + Storybook 文档 + Jest 测试
- 发布流程 = 代码提交 → 自动测试 → 构建 → 版本发布 → 文档更新
记忆口诀: "组件库开发流程长,设计开发测试忙;组件设计要单一,样式隔离很重要;Monorepo 管理代码,Rollup 打包体积小;Storybook 写文档,Jest 测试不可少;语义化版本要遵守,发布流程自动化。"
企业级架构:
├── packages/
│ ├── components/ # 组件源码
│ │ ├── button/ # Button 组件
│ │ ├── input/ # Input 组件
│ │ └── index.js # 组件导出
│ ├── utils/ # 工具函数
│ ├── styles/ # 全局样式
│ └── docs/ # 文档系统
├── rollup.config.js # 打包配置
├── jest.config.js # 测试配置
└── package.json # 项目配置2. 组件库打包与发布
面试题:如何优化组件库的打包体积?请实现一个组件库的按需加载功能。
核心考点:
- 按需加载实现(Tree Shaking)
- 打包格式(ES Module/CJS/UMD)
- 体积优化策略
- 按需导入插件
常见坑点:
- Tree Shaking 不生效:未使用 ES Module 或存在副作用
- 按需加载配置错误:需要配合 babel-plugin-import 等插件
- 样式按需导入的问题:需要将样式文件分离
- 打包产物的兼容性:需要考虑不同环境的兼容性
易错点:
- 误认为所有组件库都支持自动按需加载
- 忽略 package.json 中的 sideEffects 配置
- 对按需加载的实现原理理解错误
联合记忆:
- 按需加载 = ES Module + Tree Shaking + 按需导入插件
- 体积优化 = Tree Shaking + 代码分割 + 压缩
- 发布 = npm publish + 语义化版本 + CHANGELOG
记忆口诀: "组件库打包要优化,按需加载是关键;ES Module 支持 Tree Shaking,sideEffects 配置很重要;样式文件要分离,按需导入才有效;打包格式多,ES/CJS/UMD 都要有。"
核心配置:
// Rollup 组件库打包配置
export default {
input: 'src/index.js',
output: [
{
file: 'dist/index.esm.js',
format: 'es',
sourcemap: true
},
{
file: 'dist/index.cjs.js',
format: 'cjs',
sourcemap: true
}
],
plugins: [
resolve(),
commonjs(),
babel({
exclude: 'node_modules/**'
}),
// 样式提取
postcss({
extract: true,
modules: false,
plugins: [
require('autoprefixer')
]
})
],
external: ['vue', 'react'] // 外部依赖
};
// package.json 配置
{
"name": "my-component-library",
"version": "1.0.0",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"sideEffects": ["*.css", "*.scss"], // 样式文件有副作用
"files": ["dist", "src"]
}七、浏览器工作原理
1. 浏览器渲染流程
面试题:请详细描述浏览器的渲染流程,包括关键渲染路径的优化策略。
核心考点:
- 关键渲染路径(HTML → CSSOM → DOM Tree → Render Tree → Layout → Paint → Composite)
- 重排(Reflow)与重绘(Repaint)
- 渲染优化策略
- GPU 加速
常见坑点:
- 阻塞渲染的资源:CSS 是阻塞渲染的,JavaScript 也会阻塞渲染
- 过度重排重绘:频繁修改 DOM 或样式会导致性能问题
- CSS 选择器的性能问题:复杂的 CSS 选择器会影响渲染性能
- 字体加载的闪烁问题:FOIT(Flash of Invisible Text)
易错点:
- 混淆重排与重绘
- 忽略关键渲染路径的优化
- 对 CSSOM 与 DOM 的构建顺序理解错误
- 忽略字体加载策略(font-display)
联合记忆:
- 渲染流程 = DOM 构建 → CSSOM 构建 → Render Tree 构建 → 布局 → 绘制 → 合成
- 优化策略 = 减少阻塞资源 + 优化 CSSOM + 最小化重排重绘 + GPU 加速
- 关键指标 = First Paint (FP) + First Contentful Paint (FCP) + Largest Contentful Paint (LCP)
记忆口诀: "浏览器渲染流程多,DOM CSSOM 先构建;Render Tree 来合成,布局绘制再合成;重排重绘影响性能,减少操作要注意;关键路径要优化,阻塞资源先处理;GPU 加速效果好,transform opacity 来使用。"
优化策略:
<!-- 优化关键渲染路径的 HTML -->
<!DOCTYPE html>
<html>
<head>
<!-- 异步加载 CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
<!-- 异步加载 JavaScript -->
<script src="script.js" async></script>
</head>
<body>
<!-- 关键内容 -->
<div class="hero">...</div>
<!-- 非关键内容延迟加载 -->
<div class="secondary" loading="lazy">...</div>
</body>
</html>2. 浏览器事件循环
面试题:请详细描述浏览器的事件循环机制,包括宏任务与微任务的执行顺序。
核心考点:
- 事件循环的概念
- 宏任务与微任务的区别
- 执行顺序
- 异步编程的实现
常见坑点:
- 微任务与宏任务的执行顺序:微任务在当前宏任务执行完成后立即执行
- Promise 的状态变化时机:Promise 的 resolve/reject 是同步的,但 then/catch 是微任务
- async/await 的执行顺序:await 后面的代码会被放入微任务队列
- 定时器的精度问题:setTimeout 的延迟时间不是精确的
易错点:
- 混淆浏览器与 Node.js 的事件循环
- 忽略微任务的优先级
- 对 Promise 的执行时机理解错误
- 误认为 async/await 会创建新的事件循环
联合记忆:
- 事件循环 = 调用栈 + 任务队列(宏任务/微任务)
- 宏任务 = setTimeout/setInterval/I/O/DOM 事件
- 微任务 = Promise.then/catch/finally/async/await/process.nextTick
- 执行顺序 = 调用栈清空 → 微任务队列清空 → 宏任务队列取出一个执行 → 重复
记忆口诀: "事件循环机制好,异步编程全靠它;宏任务微任务要分清,执行顺序有讲究;调用栈空微任务清,然后执行宏任务;Promise 同步状态变,then/catch 是微任务;async/await 语法糖,微任务队列来执行。"
执行顺序示例:
console.log('1'); // 同步代码
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
async function asyncFunc() {
await Promise.resolve();
console.log('4'); // 微任务(await 后面的代码)
}
asyncFunc();
console.log('5'); // 同步代码
// 输出顺序:1, 5, 3, 4, 2八、前端工程化与性能优化
1. 前端工程化核心
面试题:请描述一个完整的前端工程化流程,包括代码规范、自动化测试、构建部署等环节。
核心考点:
- 代码规范(ESLint/Prettier/EditorConfig)
- 自动化测试(单元测试/集成测试/E2E 测试)
- 构建工具(Webpack/Rollup/Vite)
- CI/CD 流水线
- 代码质量保障
常见坑点:
- 代码规范执行不严格:缺乏 Git Hooks 强制执行
- 测试覆盖率不足:未设置合理的测试覆盖率阈值
- 构建配置过于复杂:缺乏文档和维护
- CI/CD 流水线不稳定:构建过程中频繁失败
易错点:
- 混淆 ESLint 与 Prettier 的作用
- 忽略 Git Hooks 的配置(Husky/Lint-Staged)
- 对测试金字塔(单元测试 > 集成测试 > E2E 测试)理解错误
- 未设置合理的构建缓存
联合记忆:
- 工程化 = 代码规范 + 自动化测试 + 构建工具 + CI/CD
- 质量保障 = 代码审查 + 自动化测试 + 性能监控
- 部署流程 = 构建 → 测试 → 部署 → 监控
记忆口诀: "前端工程化流程全,代码规范是起点;ESLint 查错误,Prettier 格式化;Git Hooks 来强制,Husky Lint-Staged 配合好;自动化测试不能少,单元集成 E2E;构建工具选得对,Webpack Rollup Vite 任你挑;CI/CD 流水线,代码提交自动跑;质量保障多维度,性能监控不能少。"
企业级配置:
// package.json 工程化配置
{
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint src --ext .js,.ts,.vue",
"lint:fix": "eslint src --ext .js,.ts,.vue --fix",
"test": "jest",
"test:coverage": "jest --coverage",
"precommit": "lint-staged",
"prepare": "husky install"
},
"devDependencies": {
"eslint": "^8.0.0",
"prettier": "^2.0.0",
"husky": "^7.0.0",
"lint-staged": "^11.0.0",
"jest": "^27.0.0",
"vite": "^2.0.0"
},
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write"
]
}
}2. 性能优化策略
面试题:请详细描述前端性能优化的策略,包括加载优化、渲染优化、运行优化等方面。
核心考点:
- 加载性能优化
- 渲染性能优化
- 运行性能优化
- 性能监控与分析
- Core Web Vitals
常见坑点:
- 过度优化:优化非关键路径的资源
- 忽略性能监控:未设置性能监控指标
- 图片优化不足:未使用现代图片格式(WebP/AVIF)或懒加载
- 缓存策略不当:未设置合理的缓存过期时间
易错点:
- 混淆不同类型的性能指标(FP/FCP/LCP/FID/TBT/CLS)
- 忽略关键渲染路径的优化
- 对 Service Worker 的缓存策略理解错误
- 未考虑移动端性能优化
联合记忆:
- 加载优化 = 资源压缩 + CDN + 缓存策略 + 懒加载 + 预加载
- 渲染优化 = 关键渲染路径优化 + 减少重排重绘 + GPU 加速
- 运行优化 = 代码分割 + Tree Shaking + 内存泄漏监控 + 异步处理
- 性能指标 = Core Web Vitals(LCP/FID/CLS)
记忆口诀: "前端性能优化多,加载渲染运行说;加载优化压缩资源用 CDN,缓存策略要合理;懒加载预加载,关键资源先加载;渲染优化减少重排重绘,GPU 加速效果好;运行优化代码分割,Tree Shaking 清除没用代码;性能监控 Core Web Vitals,LCP FID CLS 要达标。"
企业级优化:
<!-- 加载优化 -->
<!-- 1. 资源压缩与 CDN -->
<script src="https://cdn.example.com/script.min.js"></script>
<!-- 2. 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.jpg" as="image">
<!-- 3. 懒加载非关键资源 -->
<img src="lazy.jpg" loading="lazy">
<!-- 渲染优化 -->
<!-- 1. 关键 CSS 内联 -->
<style>
.hero { background: #000; }
</style>
<!-- 2. 使用 CSS 动画替代 JavaScript 动画 -->
<div class="animate"></div>
<style>
.animate { transition: transform 0.3s ease; }
.animate:hover { transform: scale(1.1); }
</style>
<!-- 3. 减少重排重绘 -->
<div class="container"></div>
<script>
const container = document.querySelector('.container');
// 使用 DocumentFragment 批量更新 DOM
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
container.appendChild(fragment);
</script>3. 打包优化策略
面试题:如何优化 Webpack 的构建性能和打包体积?请结合具体配置说明。
核心考点:
- 构建性能优化(缓存、多线程、依赖优化)
- 打包体积优化(Tree Shaking、Code Splitting、压缩)
- 依赖分析
- 懒加载
常见坑点:
- 缓存配置不当:未启用 babel-loader 或 webpack 缓存
- 多线程构建的过度使用:小项目启用多线程可能会降低性能
- Code Splitting 配置错误:导致 chunk 过多或过大
- Tree Shaking 不生效:未使用 ES Module 或存在副作用
易错点:
- 混淆 optimization.splitChunks 与 optimization.runtimeChunk
- 忽略 externals 配置(将第三方库从打包文件中排除)
- 对 Tree Shaking 的实现原理理解错误
- 未分析打包结果(使用 webpack-bundle-analyzer)
联合记忆:
- 构建性能 = 缓存 + 多线程 + 依赖优化 + 减少解析
- 打包体积 = Tree Shaking + Code Splitting + 压缩 + externals
- 分析工具 = webpack-bundle-analyzer + source-map-explorer
记忆口诀: "Webpack 优化两方面,构建性能打包体积;构建性能用缓存,多线程构建速度快;依赖优化 externals,减少打包文件大小;Tree Shaking 清除无用代码,Code Splitting 分割代码;压缩工具不能少,bundle 分析要做好。"
企业级配置:
// Webpack 优化配置
module.exports = {
// 1. 构建性能优化
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 排除 node_modules 目录,避免重复编译第三方库
use: [
{
loader: 'thread-loader', // 使用 thread-loader 实现多线程编译
options: {
workers: 2 // 指定线程数量为 2,根据 CPU 核心数调整
}
},
{
loader: 'babel-loader', // 使用 babel-loader 转译 JavaScript
options: {
cacheDirectory: true, // 启用缓存目录,缓存编译结果以提高后续构建速度
cacheCompression: false // 禁用缓存压缩,减少 CPU 开销
}
}
]
}
]
},
// 2. 打包体积优化
optimization: {
usedExports: true, // 启用 Tree Shaking,标记未使用的导出
minimize: true, // 启用代码压缩
minimizer: [
new TerserPlugin({
parallel: true, // 启用多线程压缩
extractComments: false // 不提取注释到单独的 LICENSE 文件
}),
new CssMinimizerPlugin() // 压缩 CSS 文件
],
splitChunks: {
chunks: 'all', // 对所有类型的 chunks(同步/异步)进行分割
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的文件
name: 'vendors', // 生成的 chunk 名称
chunks: 'all' // 对所有 chunks 应用此规则
},
common: {
name: 'common', // 生成的 chunk 名称
minChunks: 2, // 至少被引用 2 次才会被分割
chunks: 'all', // 对所有 chunks 应用此规则
enforce: true // 强制分割,即使不符合默认大小限制
}
}
},
runtimeChunk: {
name: 'runtime' // 将运行时代码提取到单独的 chunk
}
},
// 3. 依赖优化
externals: {
'react': 'React', // 将 react 从打包中排除,使用全局变量 React
'react-dom': 'ReactDOM' // 将 react-dom 从打包中排除,使用全局变量 ReactDOM
},
// 4. 分析工具
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static', // 生成静态 HTML 报告
openAnalyzer: false // 不自动打开报告
})
]
};配置说明:
构建性能优化:
thread-loader:启用多线程编译,提高大型项目的编译速度babel-loader缓存:避免重复编译相同的代码,显著提升二次构建速度exclude: /node_modules/:避免编译第三方依赖,减少不必要的工作
打包体积优化:
usedExports+ Tree Shaking:移除未使用的代码,减小打包体积splitChunks:将第三方库和公共代码分割成单独的文件,实现缓存复用runtimeChunk:提取运行时代码,避免因为模块变化导致整个 bundle 失效minimizer:使用 Terser 和 CSSMinimizer 压缩 JavaScript 和 CSS
依赖优化:
externals:将常用第三方库从打包文件中排除,通过 CDN 引入,减小 bundle 大小
分析工具:
BundleAnalyzerPlugin:生成可视化的打包分析报告,帮助识别体积过大的模块
解决的问题:
- 构建时间过长:通过多线程编译和缓存机制显著提升构建速度
- 打包体积过大:通过代码分割、Tree Shaking、压缩和 externals 配置减小 bundle 大小
- 缓存失效频繁:通过 runtimeChunk 和 splitChunks 优化缓存策略
- 难以定位体积问题:通过 bundle 分析工具直观地识别问题模块
九、高级应用与实践
1. 大文件上传与断点续传
面试题:如何实现大文件上传、断点续传和秒传功能?请详细描述其实现原理。
核心考点:
- 文件分片上传
- 断点续传实现
- 秒传功能
- WebSocket 通信
- 服务端配合
常见坑点:
- 文件分片的大小选择:分片过大或过小都会影响性能
- 断点续传的状态管理:需要在客户端保存上传状态
- 并发上传的控制:并发数过多会导致网络拥塞
- 服务端的文件合并:需要确保分片顺序正确
易错点:
- 忽略文件哈希计算的性能问题(可以使用 Web Worker)
- 未处理网络错误导致的上传失败
- 对服务端的文件存储策略考虑不周
- 忽略断点续传的安全性问题(如身份验证)
联合记忆:
- 大文件上传 = 文件分片 + 并发上传 + 断点续传 + 秒传
- 断点续传 = 上传状态保存 + 已上传分片记录 + 续传时跳过已上传分片
- 秒传 = 文件哈希计算 + 服务端文件存在性检查
记忆口诀: "大文件上传不好搞,分片上传是关键;断点续传要记录,已上传分片莫重复;秒传功能靠哈希,服务端检查文件存不存在;并发上传要控制,网络拥塞要避免;WebSocket 实时通信,上传进度看得见。"
核心实现:
// 大文件上传核心实现
class FileUploader {
constructor(options) {
this.options = options;
this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB 分片
this.concurrency = options.concurrency || 3; // 并发数
this.uploadingChunks = [];
this.uploadedChunks = new Set();
}
// 计算文件哈希(使用 Web Worker)
async calculateHash(file) {
return new Promise((resolve) => {
const worker = new Worker('hashWorker.js');
worker.postMessage({ file, chunkSize: this.chunkSize });
worker.onmessage = (e) => {
resolve(e.data.hash);
worker.terminate();
};
});
}
// 检查文件是否已存在(秒传)
async checkFile(fileHash) {
const response = await fetch(`${this.options.server}/check?hash=${fileHash}`);
return response.json();
}
// 获取已上传分片
async getUploadedChunks(fileHash) {
const response = await fetch(`${this.options.server}/uploaded?hash=${fileHash}`);
const { chunks } = await response.json();
this.uploadedChunks = new Set(chunks);
}
// 上传单个分片
async uploadChunk(chunk, index, fileHash) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
formData.append('hash', fileHash);
const response = await fetch(`${this.options.server}/upload`, {
method: 'POST',
body: formData
});
return response.json();
}
// 合并分片
async mergeChunks(fileHash, fileName, fileSize) {
const response = await fetch(`${this.options.server}/merge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ hash: fileHash, fileName, fileSize })
});
return response.json();
}
// 上传文件
async upload(file) {
const fileHash = await this.calculateHash(file);
// 检查是否可以秒传
const checkResult = await this.checkFile(fileHash);
if (checkResult.exists) {
this.options.onSuccess({ url: checkResult.url });
return;
}
// 获取已上传分片
await this.getUploadedChunks(fileHash);
// 计算分片数
const chunks = [];
const totalChunks = Math.ceil(file.size / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, file.size);
chunks.push(file.slice(start, end));
}
// 并发上传分片
let uploaded = 0;
const uploadQueue = [];
for (let i = 0; i < totalChunks; i++) {
if (this.uploadedChunks.has(i)) {
uploaded++;
this.options.onProgress(uploaded / totalChunks);
continue;
}
const uploadTask = () => {
return this.uploadChunk(chunks[i], i, fileHash)
.then(() => {
uploaded++;
this.options.onProgress(uploaded / totalChunks);
// 从上传队列中移除
this.uploadingChunks = this.uploadingChunks.filter(task => task !== uploadTask);
// 继续上传下一个
if (i < totalChunks - 1) {
const nextTask = () => uploadQueue.shift()?.();
this.uploadingChunks.push(nextTask);
nextTask();
}
});
};
uploadQueue.push(uploadTask);
}
// 开始并发上传
for (let i = 0; i < Math.min(this.concurrency, uploadQueue.length); i++) {
const task = () => uploadQueue.shift()?.();
this.uploadingChunks.push(task);
task();
}
// 等待所有分片上传完成
await Promise.all(this.uploadingChunks);
// 合并分片
const result = await this.mergeChunks(fileHash, file.name, file.size);
this.options.onSuccess(result);
}
// 暂停上传
pause() {
this.uploadingChunks.forEach(task => task.abort?.());
this.uploadingChunks = [];
}
}2. 虚拟列表实现
面试题:如何实现一个高性能的虚拟列表?请详细描述其实现原理和优化策略。
核心考点:
- 虚拟列表的设计原理
- 滚动事件处理
- 可视区域计算
- 性能优化
- 边界处理
常见坑点:
- 滚动位置计算错误:导致列表内容偏移
- 可视区域计算不准确:导致显示不全或空白
- 滚动事件的性能问题:未使用节流或防抖
- 动态高度的处理:固定高度的虚拟列表无法处理动态高度
易错点:
- 忽略滚动事件的默认行为
- 未考虑滚动容器的 padding 和 margin
- 对虚拟列表的缓存策略考虑不周
- 忽略虚拟列表的可访问性(A11y)
联合记忆:
- 虚拟列表 = 只渲染可视区域内的元素 + 滚动时动态更新
- 核心计算 = 可视区域高度 + 元素高度 + 滚动位置
- 优化策略 = 节流滚动事件 + 元素缓存 + 动态高度计算
记忆口诀: "虚拟列表性能好,只渲染可视区域元素;滚动事件要节流,避免频繁计算;可视区域高度元素高度滚动位置,三者计算不可少;动态高度要处理,元素缓存提高性能;边界情况要考虑,空白填充要准确。"
核心实现:
// 虚拟列表组件实现(React)
function VirtualList({ items, itemHeight, containerHeight, overscan = 5 }) {
const [scrollTop, setScrollTop] = useState(0);
// 容器引用
const containerRef = useRef(null);
// 计算可视区域内的元素
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(items.length - 1, Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan);
// 可视区域内的元素
const visibleItems = items.slice(startIndex, endIndex + 1);
// 偏移量
const offsetY = startIndex * itemHeight;
// 总高度
const totalHeight = items.length * itemHeight;
// 滚动事件处理(节流)
const handleScroll = useCallback(
throttle((e) => {
setScrollTop(e.target.scrollTop);
}, 16),
[]
);
return (
<div
ref={containerRef}
className="virtual-list-container"
style={{ height: containerHeight, overflowY: 'auto' }}
onScroll={handleScroll}
>
<div className="virtual-list-wrapper" style={{ height: totalHeight }}>
<div className="virtual-list-content" style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div
key={item.id}
className="virtual-list-item"
style={{ height: itemHeight }}
>
{item.content}
</div>
))}
</div>
</div>
</div>
);
}
// 节流函数
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return fn.apply(this, args);
};
}3. Axios 二次封装
面试题:如何二次封装 Axios?请实现一个包含请求拦截、响应拦截、错误处理、取消请求等功能的 Axios 实例。
核心考点:
- Axios 拦截器
- 请求/响应处理
- 错误处理机制
- 取消请求
- 超时处理
常见坑点:
- 请求拦截器的配置顺序错误:拦截器的执行顺序是后进先出
- 响应拦截器的错误处理不当:未正确区分网络错误和业务错误
- 取消请求的实现:需要为每个请求创建取消令牌
- 超时处理的配置:全局超时和请求级超时的优先级
易错点:
- 混淆 Axios 的实例方法和静态方法
- 忽略响应数据的格式一致性
- 对 Axios 的错误对象结构理解错误
- 未处理跨域请求的配置
联合记忆:
- Axios 封装 = 实例创建 + 请求拦截 + 响应拦截 + 错误处理 + 取消请求
- 拦截器 = 请求拦截(配置修改、添加 token) + 响应拦截(数据处理、错误统一处理)
- 错误处理 = 网络错误 + 业务错误 + 超时错误
记忆口诀: "Axios 封装好处多,请求响应拦截器;请求拦截加 token,响应拦截处理数据;错误处理要统一,网络业务超时分清楚;取消请求令牌用,超时配置要合理;实例创建多环境,配置灵活易维护。"
企业级实现:
// Axios 二次封装
import axios from 'axios';
import { ElMessage, ElLoading } from 'element-plus';
// 创建 Axios 实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 加载实例
let loadingInstance = null;
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 1. 显示加载动画
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
});
// 2. 添加 token
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 3. 处理取消请求
if (config.cancelToken) {
// 用户自定义的取消令牌
} else {
// 自动创建取消令牌
const CancelToken = axios.CancelToken;
config.cancelToken = new CancelToken((cancel) => {
config.cancel = cancel;
});
}
// 4. 处理请求参数
if (config.method === 'get') {
// get 请求参数处理
config.params = { ...config.params, _t: Date.now() };
}
return config;
},
(error) => {
// 请求错误处理
loadingInstance?.close();
ElMessage.error('请求配置错误');
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
// 关闭加载动画
loadingInstance?.close();
// 处理响应数据
const res = response.data;
// 1. 统一错误处理
if (res.code !== 200) {
// 业务错误
ElMessage.error(res.message || '请求失败');
// 特殊错误处理
if (res.code === 401) {
// 未授权,跳转到登录页
localStorage.removeItem('token');
window.location.href = '/login';
} else if (res.code === 403) {
// 权限不足
ElMessage.error('权限不足,无法访问');
}
return Promise.reject(new Error(res.message || 'Error'));
}
// 2. 成功响应
return res;
},
(error) => {
// 关闭加载动画
loadingInstance?.close();
// 错误分类处理
if (axios.isCancel(error)) {
// 取消请求
console.log('Request canceled:', error.message);
return Promise.reject(error);
}
// 网络错误或服务器错误
let message = '请求失败,请稍后重试';
if (error.response) {
// 服务器返回错误
const { status, data } = error.response;
switch (status) {
case 400:
message = '请求参数错误';
break;
case 401:
message = '未授权,请重新登录';
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
message = '权限不足,无法访问';
break;
case 404:
message = '请求资源不存在';
break;
case 500:
message = '服务器内部错误';
break;
case 502:
message = '网关错误';
break;
case 503:
message = '服务不可用';
break;
case 504:
message = '网关超时';
break;
default:
message = data.message || `请求失败(${status})`;
}
} else if (error.request) {
// 请求已发送但没有收到响应
message = '网络错误,请检查网络连接';
} else {
// 请求配置错误
message = error.message;
}
ElMessage.error(message);
return Promise.reject(error);
}
);
// 导出请求方法
export const request = service;
// 导出常用请求方法
export const get = (url, params, config = {}) => {
return service.get(url, { params, ...config });
};
export const post = (url, data, config = {}) => {
return service.post(url, data, config);
};
export const put = (url, data, config = {}) => {
return service.put(url, data, config);
};
export const del = (url, params, config = {}) => {
return service.delete(url, { params, ...config });
};
export default service;
### 4. Vue 自定义指令
**面试题**:如何自定义一个 Vue 指令?请实现一个防抖点击指令和一个权限控制指令。
**核心考点**:
- 自定义指令的生命周期钩子
- 指令的参数和修饰符
- 指令的作用域和数据传递
- 指令的应用场景
**常见坑点**:
1. **指令钩子函数的参数使用错误**:未正确使用 el/binding/vnode/oldVnode 参数
2. **指令的内存泄漏**:未在 unbind 钩子中清理事件监听器
3. **指令的作用域理解错误**:指令中的 this 指向不是组件实例
4. **指令的参数传递错误**:未正确处理 binding.value 和 binding.expression
**易错点**:
- 混淆全局指令和局部指令的注册方式
- 忽略指令的更新钩子(update/componentUpdated)
- 对指令的优先级理解错误
- 未考虑指令的兼容性(Vue 2 vs Vue 3)
**联合记忆**:
- Vue 2 指令钩子 = bind/inserted/update/componentUpdated/unbind
- Vue 3 指令钩子 = beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted
- 应用场景 = DOM 操作 + 权限控制 + 表单验证 + 交互增强
**记忆口诀**:
"Vue 自定义指令强,DOM 操作很方便;钩子函数生命周期,bind inserted update unbind;参数修饰符要会用,内存泄漏要清理;防抖权限验证好,指令应用场景多。"
**企业级实现**:
```javascript
// Vue 2 自定义指令
// 1. 防抖点击指令
Vue.directive('debounce', {
bind(el, binding) {
let timer = null;
el.handler = function() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
binding.value.apply(this, arguments);
}, binding.arg || 300);
};
el.addEventListener('click', el.handler);
},
unbind(el) {
el.removeEventListener('click', el.handler);
}
});
// 2. 权限控制指令
Vue.directive('permission', {
inserted(el, binding) {
const userPermissions = store.getters.permissions;
const requiredPermission = binding.value;
if (!userPermissions.includes(requiredPermission)) {
el.style.display = 'none';
// 或者直接移除元素
// el.parentNode.removeChild(el);
}
},
update(el, binding) {
const userPermissions = store.getters.permissions;
const requiredPermission = binding.value;
if (userPermissions.includes(requiredPermission)) {
el.style.display = '';
} else {
el.style.display = 'none';
}
}
});
// Vue 3 自定义指令
const debounce = {
mounted(el, binding) {
let timer = null;
el.handler = function() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
binding.value.apply(this, arguments);
}, binding.arg || 300);
};
el.addEventListener('click', el.handler);
},
unmounted(el) {
el.removeEventListener('click', el.handler);
}
};
// 在组件中使用
app.directive('debounce', debounce);5. 权限显隐与动态路由
面试题:如何实现前端权限控制?请详细描述路由权限、菜单权限和按钮权限的实现方案。
核心考点:
- 权限控制的分级(路由/菜单/按钮)
- 动态路由生成
- 权限判断的时机
- 权限信息的存储
- 权限的动态更新
常见坑点:
- 权限控制的安全性问题:前端权限控制不能替代后端权限控制
- 动态路由的生成错误:路由配置错误导致页面无法访问
- 权限信息的存储安全:敏感权限信息不应存储在本地存储
- 权限更新的时机问题:用户角色变化时未及时更新权限
易错点:
- 误认为前端权限控制足够安全(需要后端配合)
- 忽略权限控制的性能影响
- 对动态路由的异步加载处理不当
- 未考虑权限控制的可扩展性
联合记忆:
- 权限控制 = 前端控制 + 后端验证
- 实现方案 = 路由守卫 + 动态路由 + 权限指令 + 权限过滤器
- 权限信息 = 角色 + 资源 + 操作
记忆口诀: "前端权限控制三级,路由菜单按钮齐;路由守卫控制访问,动态路由按需加载;菜单权限控制显示,按钮权限用指令;前端控制做过滤,后端验证是关键;权限信息安全存,动态更新要及时。"
企业级实现:
// Vue 权限控制实现
// 1. 路由配置(包含权限信息)
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard'),
meta: { requiresAuth: true, permission: 'dashboard:view' }
},
{
path: '/user',
component: () => import('@/views/User'),
meta: { requiresAuth: true, permission: 'user:view' },
children: [
{
path: 'list',
component: () => import('@/views/User/List'),
meta: { permission: 'user:list' }
},
{
path: 'add',
component: () => import('@/views/User/Add'),
meta: { permission: 'user:add' }
}
]
}
];
// 2. 路由守卫
router.beforeEach((to, from, next) => {
// 检查是否需要认证
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token');
if (!token) {
return next('/login');
}
// 检查权限
if (to.meta.permission) {
const userPermissions = store.getters.permissions;
if (!userPermissions.includes(to.meta.permission)) {
return next('/403');
}
}
}
next();
});
// 3. 动态路由生成
const generateRoutes = (userPermissions) => {
const accessibleRoutes = [];
const checkPermission = (route) => {
// 检查路由是否有权限访问
if (route.meta && route.meta.permission) {
return userPermissions.includes(route.meta.permission);
}
return true;
};
const filterRoutes = (routes) => {
return routes.filter(route => {
const canAccess = checkPermission(route);
if (canAccess && route.children) {
route.children = filterRoutes(route.children);
}
return canAccess;
});
};
return filterRoutes(routes);
};
// 4. 菜单生成
const generateMenus = (userPermissions) => {
const menus = [];
const filterMenus = (routes) => {
return routes.map(route => {
if (route.meta && route.meta.permission && !userPermissions.includes(route.meta.permission)) {
return null;
}
const menu = {
path: route.path,
name: route.meta.title,
icon: route.meta.icon
};
if (route.children) {
const children = filterMenus(route.children);
if (children.length) {
menu.children = children;
}
}
return menu;
}).filter(Boolean);
};
return filterMenus(routes);
};
// 5. 权限指令
Vue.directive('permission', {
inserted(el, binding) {
const userPermissions = store.getters.permissions;
const requiredPermission = binding.value;
if (!userPermissions.includes(requiredPermission)) {
el.style.display = 'none';
}
}
});6. SWC 编译器
面试题:什么是 SWC?它相比 Babel 有哪些优势?请说明如何在项目中配置 SWC。
核心考点:
- SWC 编译器的原理
- SWC 与 Babel 的对比
- SWC 的配置方式
- SWC 在构建工具中的应用
常见坑点:
- SWC 配置的兼容性问题:SWC 的配置与 Babel 不完全兼容
- 插件生态的缺失:SWC 的插件生态不如 Babel 丰富
- TypeScript 支持的局限性:某些 TypeScript 特性可能不被支持
- 构建工具的集成问题:与 Webpack/Rollup/Vite 的集成配置
易错点:
- 误认为 SWC 可以完全替代 Babel(目前还不能)
- 忽略 SWC 的版本差异
- 对 SWC 的编译目标配置错误
- 未考虑 SWC 的浏览器兼容性
联合记忆:
- SWC = Rust 编写的超高速 JavaScript/TypeScript 编译器
- 优势 = 编译速度快 + 内存占用低 + 支持 TypeScript/JSX
- 应用场景 = 构建工具替代 Babel + 代码压缩 + 类型检查
记忆口诀: "SWC 编译器真快,Rust 编写性能高;编译速度超 Babel,内存占用也很少;支持 TypeScript JSX,配置简单易上手;构建工具集成好,Webpack Vite Rollup 都支持;插件生态待完善,部分场景需 Babel。"
企业级配置:
// 1. Vite 中使用 SWC
// vite.config.js
import { defineConfig } from 'vite'; // 导入 Vite 的配置函数
import vue from '@vitejs/plugin-vue'; // 导入 Vue 插件
import swc from 'vite-plugin-swc'; // 导入 SWC 插件
export default defineConfig({
plugins: [
vue(), // 使用 Vue 插件处理 .vue 文件
swc({
// SWC 配置
jsc: {
parser: {
syntax: 'typescript', // 解析 TypeScript 语法
tsx: false, // 不解析 TSX 语法(Vue 项目使用 JSX 时可设为 true)
decorators: true // 支持装饰器语法
},
target: 'es2020' // 编译目标为 ES2020
}
})
]
});
// 2. Webpack 中使用 SWC
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/, // 匹配 JavaScript/TypeScript 文件
exclude: /node_modules/, // 排除 node_modules 目录
use: {
loader: 'swc-loader', // 使用 swc-loader 处理文件
options: {
jsc: {
parser: {
syntax: 'typescript', // 解析 TypeScript 语法
tsx: true, // 解析 TSX 语法
dynamicImport: true, // 支持动态导入语法
decorators: true // 支持装饰器语法
},
transform: {
react: {
runtime: 'automatic' // 使用 React 17+ 的新 JSX 转换运行时
}
},
target: 'es2020', // 编译目标为 ES2020
loose: true // 使用宽松模式,提高编译速度(兼容性略有降低)
}
}
}
}
]
}
};
// 3. SWC 配置文件 (.swcrc)
{
"jsc": {
"parser": {
"syntax": "typescript", // 解析 TypeScript 语法
"tsx": true, // 解析 TSX 语法
"decorators": true, // 支持装饰器语法
"dynamicImport": true // 支持动态导入语法
},
"transform": {
"react": {
"runtime": "automatic" // 使用 React 17+ 的新 JSX 转换运行时
}
},
"target": "es2020", // 编译目标为 ES2020
"loose": true, // 使用宽松模式,提高编译速度
"minify": {
"compress": true, // 启用代码压缩
"mangle": true // 启用变量名混淆
}
},
"minify": true // 启用代码压缩
}配置说明:
Vite 中使用 SWC:
- 替换 Vite 默认的 ESBuild,使用 SWC 处理 JavaScript/TypeScript 文件
- 支持 Vue 项目的 TypeScript 语法和装饰器
- 配置简洁,与 Vite 生态完美集成
Webpack 中使用 SWC:
- 替代 Babel 处理 JavaScript/TypeScript 文件
- 支持 React 项目的 JSX/TSX 语法
- 配置与 Babel 类似,但编译速度更快
SWC 配置文件 (.swcrc):
- 独立的 SWC 配置文件,可在多个工具间共享
- 包含完整的解析、转换和压缩配置
- 支持 TypeScript 语法和 React JSX 转换
SWC 主要参数说明:
jsc.parser.syntax:指定要解析的语法类型(typescript/javascript)jsc.parser.tsx:是否解析 JSX/TSX 语法jsc.parser.decorators:是否支持装饰器语法jsc.parser.dynamicImport:是否支持动态导入语法jsc.transform.react.runtime:指定 React JSX 转换运行时jsc.target:指定编译目标(es2015/es2018/es2020 等)jsc.loose:是否使用宽松模式,平衡编译速度和兼容性jsc.minify:配置代码压缩选项
解决的问题:
- 构建速度慢:SWC 比 Babel 快 5-20 倍,显著提升构建速度
- 内存占用高:SWC 用 Rust 编写,内存占用远低于 Babel
- 配置复杂:SWC 配置简洁,学习成本低
- 兼容性问题:支持广泛的 JavaScript/TypeScript 语法和特性
7. 长表单应用的高性能存储与同步
面试题:如何实现一个高性能的长表单应用,包括表单数据的本地存储、自动保存和数据同步?
核心考点:
- 表单数据的本地存储策略
- 自动保存机制
- 数据同步策略
- 表单性能优化
- 数据验证和错误处理
常见坑点:
- 本地存储的性能问题:频繁读写本地存储导致性能下降
- 自动保存的时机错误:保存过于频繁或不及时
- 数据同步的冲突处理:多设备同步时的数据冲突
- 表单验证的性能影响:实时验证导致的性能问题
易错点:
- 混淆不同本地存储方案的优缺点(localStorage/sessionStorage/IndexedDB)
- 忽略本地存储的容量限制
- 对数据同步的实时性要求过高
- 未考虑表单数据的安全性
联合记忆:
- 存储方案 = localStorage(小数据) + IndexedDB(大数据)
- 自动保存 = 防抖 + 定时保存 + 页面离开保存
- 数据同步 = 增量同步 + 冲突检测 + 版本控制
- 性能优化 = 虚拟滚动 + 延迟加载 + 批量处理
记忆口诀: "长表单应用挑战多,存储同步性能说;本地存储选方案,localStorage 小数据,IndexedDB 大数据;自动保存用防抖,定时保存加离开保存;数据同步增量传,冲突处理版本控;表单性能优化好,虚拟滚动延迟加载;验证错误要处理,用户体验很重要。"
企业级实现:
// 长表单应用实现
class FormManager {
constructor(options) {
this.options = options;
this.formData = {};
this.saveTimer = null;
this.syncTimer = null;
this.version = 0;
this.storageKey = options.storageKey || 'form-data';
// 初始化
this.init();
}
// 初始化
init() {
// 从本地存储加载数据
this.loadFromStorage();
// 设置表单事件监听
this.setupEventListeners();
// 设置自动保存
this.setupAutoSave();
// 设置数据同步
this.setupSync();
}
// 从本地存储加载数据
loadFromStorage() {
try {
const savedData = localStorage.getItem(this.storageKey);
if (savedData) {
const { data, version } = JSON.parse(savedData);
this.formData = data;
this.version = version;
this.options.onDataLoaded?.(this.formData);
}
} catch (error) {
console.error('Failed to load form data from storage:', error);
}
}
// 保存到本地存储
saveToStorage() {
try {
const dataToSave = {
data: this.formData,
version: this.version,
timestamp: Date.now()
};
localStorage.setItem(this.storageKey, JSON.stringify(dataToSave));
this.options.onSave?.();
} catch (error) {
console.error('Failed to save form data to storage:', error);
}
}
// 设置表单事件监听
setupEventListeners() {
// 监听表单输入事件(防抖)
const handleInput = debounce((field, value) => {
this.updateField(field, value);
}, 300);
// 监听页面离开事件
window.addEventListener('beforeunload', () => {
this.saveToStorage();
});
// 监听窗口关闭事件
window.addEventListener('unload', () => {
this.saveToStorage();
});
// 暴露给外部调用
this.handleInput = handleInput;
}
// 设置自动保存
setupAutoSave() {
// 定时保存(每5分钟)
this.saveTimer = setInterval(() => {
this.saveToStorage();
}, 5 * 60 * 1000);
}
// 设置数据同步
setupSync() {
// 定时同步(每10分钟)
this.syncTimer = setInterval(() => {
this.syncData();
}, 10 * 60 * 1000);
}
// 更新表单字段
updateField(field, value) {
this.formData[field] = value;
this.version++;
// 触发数据更新事件
this.options.onDataUpdate?.(field, value, this.formData);
}
// 手动保存
save() {
this.saveToStorage();
}
// 手动同步
async sync() {
await this.syncData();
}
// 数据同步
async syncData() {
try {
// 检查是否有未同步的数据
const savedData = localStorage.getItem(this.storageKey);
if (!savedData) return;
const { data, version, timestamp } = JSON.parse(savedData);
// 发送同步请求
const response = await fetch('/api/form/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
data,
version,
timestamp,
formId: this.options.formId
})
});
const result = await response.json();
// 处理同步结果
if (result.success) {
// 更新本地版本
this.version = result.version;
// 处理服务器返回的最新数据
if (result.data) {
this.formData = result.data;
this.options.onDataLoaded?.(this.formData);
}
this.options.onSyncSuccess?.();
} else if (result.conflict) {
// 处理数据冲突
this.handleConflict(result.serverData);
}
} catch (error) {
console.error('Failed to sync form data:', error);
this.options.onSyncError?.(error);
}
}
// 处理数据冲突
handleConflict(serverData) {
// 显示冲突提示
this.options.onConflict?.(this.formData, serverData, (resolveOption) => {
if (resolveOption === 'useLocal') {
// 使用本地数据,重新同步
this.syncData();
} else if (resolveOption === 'useServer') {
// 使用服务器数据
this.formData = serverData.data;
this.version = serverData.version;
this.saveToStorage();
this.options.onDataLoaded?.(this.formData);
} else if (resolveOption === 'merge') {
// 合并数据
this.formData = { ...serverData.data, ...this.formData };
this.version = serverData.version + 1;
this.saveToStorage();
this.syncData();
}
});
}
// 提交表单
async submit() {
try {
// 保存数据
this.saveToStorage();
// 同步数据
await this.syncData();
// 提交表单
const response = await fetch('/api/form/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: this.formData,
formId: this.options.formId,
version: this.version
})
});
const result = await response.json();
if (result.success) {
// 提交成功,清除本地存储
this.clearStorage();
this.options.onSubmitSuccess?.(result);
} else {
this.options.onSubmitError?.(result.error);
}
} catch (error) {
console.error('Failed to submit form:', error);
this.options.onSubmitError?.(error);
}
}
// 清除本地存储
clearStorage() {
localStorage.removeItem(this.storageKey);
this.formData = {};
this.version = 0;
}
// 销毁
destroy() {
// 清除定时器
if (this.saveTimer) {
clearInterval(this.saveTimer);
}
if (this.syncTimer) {
clearInterval(this.syncTimer);
}
// 保存数据
this.saveToStorage();
}
}
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}8. 防盗链优化
面试题:如何实现网站资源的防盗链保护?请详细描述其原理和实现方案。
核心考点:
- 防盗链的工作原理
- Referer 验证
- Token 验证
- 时间戳验证
- CDN 防盗链
常见坑点:
- Referer 验证的局限性:Referer 可以被伪造
- Token 验证的安全性:Token 生成和验证的安全性
- 时间戳验证的精度问题:时间同步问题导致验证失败
- CDN 防盗链的配置错误:配置错误导致资源无法访问
易错点:
- 误认为防盗链可以完全阻止资源盗用
- 忽略合法的跨域请求
- 对 Token 过期时间设置不合理
- 未考虑 HTTPS 环境下的防盗链
联合记忆:
- 防盗链 = Referer 验证 + Token 验证 + 时间戳验证
- 实现方案 = 服务端验证 + CDN 配置 + 前端处理
- 安全性 = 多层验证 + 定期更新策略
记忆口诀: "防盗链保护资源,Referer Token 时间戳;Referer 验证简单,容易被伪造;Token 验证安全,生成算法要复杂;时间戳防滥用,有效期要合理;CDN 防盗链配置,白名单要设置;多层验证更安全,定期更新策略好。"
企业级实现:
// 服务端防盗链实现(Node.js)
// 1. Referer 验证
app.use((req, res, next) => {
// 需要防盗链的资源类型
const protectedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.pdf'];
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
// 检查是否为需要保护的资源
const isProtectedResource = protectedExtensions.some(ext =>
pathname.toLowerCase().endsWith(ext)
);
if (isProtectedResource) {
// 检查 Referer
const referer = req.headers.referer || req.headers.referrer;
if (referer) {
try {
const refererUrl = new URL(referer);
const allowedDomains = ['example.com', 'cdn.example.com'];
// 检查 Referer 是否来自允许的域名
if (!allowedDomains.includes(refererUrl.hostname)) {
// 返回 403 或默认图片
return res.status(403).send('Forbidden');
// 或返回默认图片
// return res.sendFile(path.join(__dirname, 'public', 'default.jpg'));
}
} catch (error) {
// Referer 格式错误,返回 403
return res.status(403).send('Forbidden');
}
} else {
// 没有 Referer,可能是直接访问
// 可以允许或拒绝
return res.status(403).send('Forbidden');
}
}
next();
});
// 2. Token 验证
const generateToken = (resource, expires) => {
const secret = process.env.ANTI_HOTLINK_SECRET;
const data = {
resource,
expires,
timestamp: Date.now()
};
// 使用 HMAC 生成签名
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(data));
const signature = hmac.digest('hex');
// 生成 Token
return `${signature}.${Buffer.from(JSON.stringify(data)).toString('base64')}`;
};
const validateToken = (token) => {
try {
const [signature, dataBase64] = token.split('.');
const data = JSON.parse(Buffer.from(dataBase64, 'base64').toString());
const secret = process.env.ANTI_HOTLINK_SECRET;
// 验证签名
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(data));
const expectedSignature = hmac.digest('hex');
if (signature !== expectedSignature) {
return false;
}
// 验证过期时间
if (Date.now() > data.expires) {
return false;
}
return data;
} catch (error) {
return false;
}
};
// Token 验证中间件
app.use('/protected', (req, res, next) => {
const token = req.query.token;
if (!token) {
return res.status(403).send('Forbidden');
}
const data = validateToken(token);
if (!data) {
return res.status(403).send('Invalid token');
}
// 验证资源路径
const resource = req.path;
if (data.resource !== resource) {
return res.status(403).send('Invalid resource');
}
next();
});
// 3. 前端生成带 Token 的资源 URL
const generateResourceUrl = (resource, expiresIn = 3600000) => {
const expires = Date.now() + expiresIn;
const token = generateToken(resource, expires);
return `${resource}?token=${token}`;
};
// 4. CDN 防盗链配置(以阿里云 CDN 为例)
// 在 CDN 控制台配置:
// - 防盗链类型:Referer 白名单
// - 允许空 Referer:是/否
// - Referer 白名单:example.com,cdn.example.com
// - 防盗链失效时间:3600 秒
// - Token 防盗链:启用
// - Token 密钥:自定义密钥
// - Token 过期时间:3600 秒
// - Token 包含的参数:path,expires,ip9. Promise 深入理解
面试题:Promise 有哪些静态方法?请实现 Promise.all、Promise.race 和 Promise.allSettled。
核心考点:
- Promise 静态方法的功能和用法
- Promise 的状态管理
- 异步流程控制
- 错误处理机制
- 并发控制
常见坑点:
- Promise.all 的错误处理:只要有一个 Promise 失败就会立即返回失败
- Promise.race 的性能问题:未正确处理快速失败的情况
- Promise.allSettled 的兼容性:旧浏览器不支持
- 自定义 Promise 实现的状态管理:状态转换错误
易错点:
- 混淆不同 Promise 静态方法的行为
- 忽略 Promise 静态方法的返回值
- 对 Promise 并发控制的实现错误
- 未考虑 Promise 静态方法的性能
联合记忆:
- Promise.all = 所有 Promise 都成功才成功,一个失败就失败
- Promise.race = 第一个完成的 Promise 决定结果
- Promise.allSettled = 所有 Promise 都完成(无论成功失败)才返回
- Promise.any = 第一个成功的 Promise 决定结果(ES2021)
记忆口诀: "Promise 静态方法多,all race allSettled any;all 全成功才成功,一个失败就失败;race 比速度,第一个完成定结果;allSettled 等全部,成功失败都返回;any 找成功,第一个成功定结果;实现这些方法要注意,状态管理异步控制。"
企业级实现:
// Promise 静态方法实现
// 1. Promise.all 实现
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
// 检查参数是否为可迭代对象
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument is not iterable'));
}
const results = [];
let completed = 0;
const total = promises.length;
// 处理空数组
if (total === 0) {
return resolve([]);
}
promises.forEach((promise, index) => {
// 确保每个元素都是 Promise
Promise.resolve(promise)
.then(value => {
results[index] = value;
completed++;
// 所有 Promise 都已完成
if (completed === total) {
resolve(results);
}
})
.catch(error => {
// 任何一个 Promise 失败就立即返回失败
reject(error);
});
});
});
};
// 2. Promise.race 实现
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
// 检查参数是否为可迭代对象
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument is not iterable'));
}
// 处理空数组
if (promises.length === 0) {
return;
}
promises.forEach(promise => {
// 确保每个元素都是 Promise
Promise.resolve(promise)
.then(value => {
resolve(value);
})
.catch(error => {
reject(error);
});
});
});
};
// 3. Promise.allSettled 实现
Promise.myAllSettled = function(promises) {
return new Promise((resolve, reject) => {
// 检查参数是否为可迭代对象
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument is not iterable'));
}
const results = [];
let completed = 0;
const total = promises.length;
// 处理空数组
if (total === 0) {
return resolve([]);
}
const processResult = (index, status, value) => {
results[index] = {
status,
[status === 'fulfilled' ? 'value' : 'reason']: value
};
completed++;
// 所有 Promise 都已完成
if (completed === total) {
resolve(results);
}
};
promises.forEach((promise, index) => {
// 确保每个元素都是 Promise
Promise.resolve(promise)
.then(value => {
processResult(index, 'fulfilled', value);
})
.catch(error => {
processResult(index, 'rejected', error);
});
});
});
};
// 4. Promise.any 实现
Promise.myAny = function(promises) {
return new Promise((resolve, reject) => {
// 检查参数是否为可迭代对象
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument is not iterable'));
}
const errors = [];
let rejected = 0;
const total = promises.length;
// 处理空数组
if (total === 0) {
return reject(new AggregateError([], 'All promises were rejected'));
}
promises.forEach((promise, index) => {
// 确保每个元素都是 Promise
Promise.resolve(promise)
.then(value => {
resolve(value);
})
.catch(error => {
errors[index] = error;
rejected++;
// 所有 Promise 都已失败
if (rejected === total) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
};10. Nginx 反向代理与负载均衡
面试题:如何配置 Nginx 实现反向代理和负载均衡?请详细描述其配置和原理。
核心考点:
- Nginx 反向代理的原理
- 负载均衡策略(轮询/权重/ip_hash/least_conn)
- Nginx 配置语法
- 健康检查和故障转移
- 性能优化配置
常见坑点:
- 反向代理的配置错误:代理路径或参数配置错误
- 负载均衡策略选择不当:未根据实际情况选择合适的策略
- 健康检查配置错误:未正确配置健康检查导致故障转移失败
- 性能优化配置不合理:过度优化或配置错误导致性能下降
易错点:
- 混淆正向代理与反向代理
- 忽略 Nginx 配置的语法错误
- 对负载均衡权重设置不合理
- 未考虑 HTTPS 环境下的反向代理
联合记忆:
- 反向代理 = 客户端 → Nginx → 后端服务器
- 负载均衡 = 轮询 + 权重 + ip_hash + least_conn
- 配置 = upstream 块 + server 块 + location 块
- 优化 = 连接池 + 缓存 + Gzip 压缩 + 限流
记忆口诀: "Nginx 反向代理好,客户端请求中间过;负载均衡分流量,轮询权重 ip_hash least_conn;upstream 块定义服务器组,server 块配置服务器;location 块处理请求,proxy_pass 指向 upstream;健康检查监控服务器,故障转移自动切;性能优化连接池,缓存压缩限流齐。"
企业级配置:
# Nginx 反向代理与负载均衡配置
# 1. 定义 upstream 服务器组
upstream backend {
# 轮询(默认)
server backend1.example.com:8080 weight=3; # 权重为 3,接收 3/6 的请求
server backend2.example.com:8080 weight=2; # 权重为 2,接收 2/6 的请求
server backend3.example.com:8080 weight=1; # 权重为 1,接收 1/6 的请求
# ip_hash 策略(同一 IP 始终转发到同一服务器)
# ip_hash;
# least_conn 策略(转发到连接数最少的服务器)
# least_conn;
# 健康检查配置
keepalive 32; # 保持连接数
keepalive_timeout 60s; # 保持连接超时时间
# 自定义健康检查(需要 ngx_http_upstream_check_module)
# check interval=3000 rise=2 fall=5 timeout=1000 type=http;
# check_http_send "HEAD /health HTTP/1.0\r\nHost: $host\r\nConnection: close\r\n\r\n";
# check_http_expect_alive http_2xx http_3xx;
}
# 2. 主配置
server {
listen 80;
server_name example.com;
# 3. 反向代理配置
location / {
# 代理设置
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 代理缓存配置
proxy_cache_bypass $http_upgrade;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_key "$scheme$request_method$host$request_uri";
# 超时配置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲区配置
proxy_buffers 8 16k;
proxy_buffer_size 32k;
}
# 4. 静态资源配置
location ~* \.(jpg|jpeg|png|gif|css|js|ico)$ {
root /var/www/html;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# 5. 健康检查端点
location /health {
proxy_pass http://backend;
proxy_set_header Host $host;
access_log off;
log_not_found off;
}
}
# 6. HTTPS 配置
server {
listen 443 ssl http2;
server_name example.com;
# SSL 证书配置
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# SSL 优化配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# HSTS 配置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 反向代理配置
location / {
proxy_pass http://backend;
# 其他代理配置同 HTTP
}
}
# 7. 负载均衡状态监控(需要 ngx_http_upstream_status_module)
location /upstream_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
# 8. 限流配置
http {
# 限流配置
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
# 应用限流
location /api {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://backend;
}
}
}11. Rollup 高级配置
面试题:Rollup 如何实现代码分割和按需加载?请详细描述其配置和原理。
核心考点:
- Rollup 代码分割的原理
- 动态导入的处理
- 输出格式的选择
- 插件的使用
- 性能优化
常见坑点:
- 代码分割配置错误:未正确配置 manualChunks 或 experimentalCodeSplitting
- 动态导入的语法错误:未使用正确的动态导入语法
- 输出格式选择不当:未根据使用场景选择合适的输出格式
- 插件版本不兼容:插件版本与 Rollup 版本不兼容
易错点:
- 混淆 Rollup 与 Webpack 的代码分割实现
- 忽略 Rollup 对 CommonJS 模块的支持
- 对 Rollup 的输出格式理解错误
- 未考虑代码分割的性能影响
联合记忆:
- 代码分割 = 动态导入 + manualChunks + experimentalCodeSplitting
- 实现方案 = 配置 manualChunks + 使用动态导入 + 选择合适的输出格式
- 优化 = Tree Shaking + 压缩 + 缓存
记忆口诀: "Rollup 代码分割好,动态导入是关键;manualChunks 自定义,experimentalCodeSplitting 实验性;输出格式选正确,es 格式支持动态导入;插件配合使用,@rollup/plugin-commonjs 处理 CommonJS;代码分割性能优,Tree Shaking 不可少。"
企业级配置:
// Rollup 高级配置
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';
import { visualizer } from 'rollup-plugin-visualizer';
import alias from '@rollup/plugin-alias';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import postcss from 'rollup-plugin-postcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
export default {
// 1. 入口配置
input: {
main: 'src/index.js',
app: 'src/app.js'
},
// 2. 输出配置
output: [
// ES Module 输出(支持代码分割和动态导入)
{
dir: 'dist/es',
format: 'es',
sourcemap: true,
entryFileNames: '[name].[hash].js',
chunkFileNames: '[name].[hash].chunk.js',
manualChunks: {
// 将第三方库打包到单独的 chunk
'vendor': ['react', 'react-dom'],
'utils': ['lodash', 'axios'],
'ui': ['antd', 'element-plus']
}
},
// CommonJS 输出
{
dir: 'dist/cjs',
format: 'cjs',
sourcemap: true,
entryFileNames: '[name].cjs.js',
chunkFileNames: '[name].[hash].chunk.cjs.js'
}
],
// 3. 插件配置
plugins: [
// 路径别名
alias({
entries: [
{ find: '@', replacement: './src' }
]
}),
// 解析 Node.js 模块
resolve({
browser: true,
preferBuiltins: false
}),
// 转换 CommonJS 模块
commonjs({
include: /node_modules/
}),
// Babel 转换
babel({
exclude: /node_modules/,
babelHelpers: 'runtime',
presets: [
['@babel/preset-env', {
targets: {
browsers: ['> 1%', 'last 2 versions']
},
modules: false
}],
['@babel/preset-react', {
runtime: 'automatic'
}]
],
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3
}]
]
}),
// 环境变量替换
replace({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
preventAssignment: true
}),
// CSS 处理
postcss({
plugins: [
autoprefixer(),
cssnano()
],
extract: true,
sourceMap: true
}),
// JSON 处理
json(),
// 代码压缩
terser({
compress: {
drop_console: true,
drop_debugger: true
},
mangle: {
toplevel: true
}
}),
// 构建分析
visualizer({
filename: 'dist/stats.html',
open: true
})
],
// 4. 外部依赖配置
external: [
'react',
'react-dom'
],
// 5. 高级配置
preserveEntrySignatures: false,
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false
},
// 6. 缓存配置
cache: true
};
// 动态导入示例
// src/app.js
import('./components/HeavyComponent').then(HeavyComponent => {
// 使用 HeavyComponent
});
// src/utils/api.js
export const fetchData = async () => {
const { default: axios } = await import('axios');
return axios.get('/api/data');
};总结
本综合面试题覆盖了前端开发的核心技术领域,包括 ES6+ 高级语法、Vue 全家桶、构建工具、Node.js 框架、CSS 预处理器、组件库开发、浏览器工作原理、前端工程化与性能优化、高级应用与实践等。
每道面试题都包含了核心考点、常见坑点、易错点、联合记忆、记忆口诀和企业级实现代码,帮助你深入理解前端开发的各个方面,为高级前端工程师面试做好充分准备。
在企业级开发实践中,需要结合具体业务场景,灵活运用这些技术和知识,注重代码质量、性能优化、用户体验和团队协作,不断提升自己的技术能力和解决问题的能力。
祝你面试成功!