哔哩哔哩-大会员中心-前端二面
📖 面试问题
1. 浏览器从输入到显示页面的全过程
- 用户在浏览器地址栏输入 URL,浏览器通过 DNS 解析获取 IP 地址。
- 浏览器与服务器建立 TCP 连接,发起 HTTP 请求。
- 服务器返回 HTML 文件,浏览器解析 HTML、CSS、JavaScript,构建 DOM 树、CSSOM 树,合并成 Render 树。
- 浏览器渲染页面,进行布局、绘制,最后显示页面。
2. TCP 三次握手过程
- 第一次握手:客户端向服务器发送 SYN 包,表示请求建立连接。
- 第二次握手:服务器收到请求后,发送 SYN-ACK 包,表示同意建立连接。
- 第三次握手:客户端收到 SYN-ACK 包后,发送 ACK 包,确认连接建立。
3. 为什么要 TCP 四次挥手
- 第一次挥手:客户端发送 FIN 包,表示没有数据发送了。
- 第二次挥手:服务器收到 FIN 包后,确认连接关闭,返回 ACK 包。
- 第三次挥手:服务器发送 FIN 包,表示服务器端也没有数据发送了。
- 第四次挥手:客户端确认收到服务器的 FIN 包后,发送 ACK 包,连接完全关闭。
4. 浏览器缓存策略
- 强缓存:通过
Cache-Control
和Expires
控制,缓存文件有效期内不会向服务器发送请求。 - 协商缓存:通过
ETag
和Last-Modified
等进行验证,决定是否从缓存中读取文件,或重新向服务器请求。
5. 讲一下 HOC(高阶组件)
- 高阶组件(HOC):是 React 的一种模式,指的是一个函数,它接受一个组件并返回一个新的组件,通常用于逻辑复用和组件装饰。
6. 登录权限判断逻辑
- 检查用户是否已经登录:查看存储的 token 或 session。
- 如果没有登录,重定向到登录页面;如果已登录,检查用户权限,决定是否允许访问某些页面。
7. Token 为什么要存在 localStorage 而不是 cookie
- localStorage:存储的数据不随每次请求自动发送,适合存储不需要随请求一起发送的数据(如 token)。
- cookie:会随着每次请求自动发送,适合存储会话信息,但会增加每次请求的负担,且存在一定的安全隐患(如 CSRF 攻击)。
8. React 动态路由怎么实现的
- 使用 React Router 的
<Route>
配置动态路径,可以通过useParams
获取动态参数。 - 例如,
<Route path="/user/:id" component={User} />
,useParams
获取id
。
9. Suspense 的实现原理
Suspense
是 React 用于处理异步渲染的组件,它会在异步操作完成之前渲染一个备用 UI(如加载动画)。React 会通过React.lazy
动态加载组件并配合Suspense
进行渲染。
10. 首屏加载优化做了哪些
- 减少首屏渲染的 JavaScript 和 CSS 文件大小,使用代码分割(Code Splitting)。
- 使用懒加载(Lazy Loading)和图片懒加载(Lazy Image)。
- 优化 WebFont 加载,使用字体显示策略(如 font-display: swap)。
- 使用服务端渲染(SSR)或静态生成(SSG)提高首屏加载速度。
✍️ 手撕题目
1. 事件总线(EventBus)
- EventBus 是一个自定义的事件管理系统,用于不同组件之间通信。
- 它通过
on
订阅事件、emit
触发事件、off
移除事件监听来实现松耦合的组件间通信。
class EventBus {
constructor() {
this.events = {};
}
on(event, fn) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(fn);
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(fn => fn(...args));
}
}
off(event, fn) {
if (!this.events[event]) return;
const index = this.events[event].indexOf(fn);
if (index !== -1) this.events[event].splice(index, 1);
}
}
2. 版本号排序
- 给定版本号 v1.0.1, v1.0.2, v1.1.0 等,要求按照版本号大小排序。
- 可以通过拆分版本号字符串并逐个比较各部分大小来进行排序。
function compareVersion(v1, v2) {
const v1Parts = v1.split('.').map(Number);
const v2Parts = v2.split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part) return 1;
if (v1Part < v2Part) return -1;
}
return 0;
}
const versions = ['v1.0.1', 'v1.1.0', 'v1.0.2'];
versions.sort(compareVersion);
console.log(versions); // ['v1.0.1', 'v1.0.2', 'v1.1.0']
3. 版本号排序 plus(如 v1.0.1-beta, v1.0.1-alpha)
- 在版本号中存在附加的后缀(如 alpha 或 beta),需要先比较数字版本部分,再比较后缀。
function compareVersionWithTag(v1, v2) {
const [v1Version, v1Tag] = v1.split('-');
const [v2Version, v2Tag] = v2.split('-');
const versionComparison = compareVersion(v1Version, v2Version);
if (versionComparison !== 0) return versionComparison;
// 若版本号相同,按后缀比较
const tags = ['alpha', 'beta', 'rc', 'stable'];
return tags.indexOf(v1Tag || 'stable') - tags.indexOf(v2Tag || 'stable');
}
const versionsWithTag = ['v1.0.1-beta', 'v1.0.1-alpha', 'v1.0.1-stable'];
versionsWithTag.sort(compareVersionWithTag);
console.log(versionsWithTag); // ['v1.0.1-alpha', 'v1.0.1-beta', 'v1.0.1-stable']