前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。专栏快速链接

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:bilibili
🕐面试时间:近期,用户上传于2026-03-20
💻面试岗位:前端开发实习生(2027届)
⏱️面试时长:30分钟(20分钟项目 + 10分钟反问)
📝面试体验:无手撕,全程深挖项目
❓面试问题:
项目深挖
- 实习项目的架构是怎样的?
- i18n(国际化)怎么设计的?
- SSR 和 ISR 的区别是什么?
- 项目中怎么优化性能的?
- 项目中前端怎么和后端交互的?
- SSE和WebSocket的区别,为什么不用WebSocket?
- SSE断线重连怎么处理?
- 后端RAG和Agent的流程了解吗?
- RAG中怎么计算chunk相似度的?
- RAG效果不好怎么优化,如果AI胡言乱语怎么办?
- 项目中树形目录编辑组件拖拉拽效果怎么实现的?每个item的数据结构是什么样的?
计算机基础
-
线程和进程的区别?
-
浏览器的线程进程有哪些?
-
浏览器不同tab是怎么通信的?
来源:牛客网 Az0424
💡 木木有话说(刷前先看)
现在很多前端岗位都在往AI全栈上去靠,这个面试内容也比较经典,RAG(检索增强生成)的内容。这说明现在的实习生要有一定的新技术自驱型,大家可以准备点全栈开发的题看看
📝 Bilibili 前端实习生·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 项目深挖型 + 原理对比型 + 跨界考察型 |
| 难度评级 | ⭐⭐⭐(三星,项目问得非常细) |
| 考察重心 | 项目架构设计、性能优化、SSR/ISR、SSE/WebSocket、RAG流程、浏览器原理 |
| 特殊之处 | 无手撕、10分钟反问、跨界问到后端AI相关内容 |
🔍 逐题深度解析
一、实习项目的架构
问题:实习项目的架构是怎样的?
这个问题考察的是你对整体项目的把控能力,不仅仅是自己写的代码,还包括整个系统的分层、技术选型、数据流向等。
javascript
// 1. 回答思路:分层讲述
- 前端层:React/Vue + SSR(Next.js/Nuxt)
- BFF层:Node.js(聚合数据、接口转发)
- 服务层:后端微服务
- 基础设施:CDN、OSS、容器化部署
// 2. 前端架构示例
src/
├── pages/ // 页面级组件,配合SSR路由
├── components/ // 公共组件
├── hooks/ // 自定义逻辑
├── services/ // API接口封装
├── stores/ // 状态管理
├── utils/ // 工具函数
├── i18n/ // 国际化配置
└── types/ // TypeScript类型
// 3. 数据流向
用户操作 -> 组件事件 -> 状态管理/API调用 -> BFF -> 后端服务
-> 更新UI <- 响应数据 <- 响应 <-
// 4. 架构设计的考量点
- 为什么这么分层?(职责分离、复用性)
- 为什么选这个技术栈?(团队熟悉度、社区生态)
- 有哪些挑战?(首屏加载、SEO、维护成本)
二、i18n怎么设计的
问题:i18n(国际化)怎么设计的?
国际化是很多实际项目都会涉及的功能,考察的是你对项目细节的把控。
javascript
// 1. 技术选型
// React项目:react-i18next
// Vue项目:vue-i18n
// 2. 目录结构
src/i18n/
├── locales/
│ ├── zh-CN/
│ │ ├── common.json
│ │ ├── home.json
│ │ └── user.json
│ └── en-US/
│ ├── common.json
│ ├── home.json
│ └── user.json
├── config.ts // i18n配置
└── index.ts // 导出实例
// 3. 核心配置
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import Backend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
i18n
.use(Backend) // 懒加载语言文件
.use(LanguageDetector) // 自动检测语言
.use(initReactI18next)
.init({
fallbackLng: 'zh-CN',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React已经安全转义
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'home', 'user'], // 命名空间
defaultNS: 'common',
})
// 4. 使用方式
// 组件内使用
import { useTranslation } from 'react-i18next'
function HomePage() {
const { t } = useTranslation('home')
return <h1>{t('title')}</h1>
}
// 5. 动态切换语言
const changeLanguage = (lng) => {
i18n.changeLanguage(lng)
// 可选:保存到localStorage
localStorage.setItem('preferredLanguage', lng)
}
// 6. SSR场景的处理
// - 需要在服务端获取用户语言偏好(从cookie/header)
// - 将语言包注入到HTML
// - 客户端激活时复用
// 7. 设计考量
- 懒加载 vs 全量加载(首屏性能 vs 切换体验)
- 命名空间拆分(按页面/模块拆分)
- 变量插值({{count}}、复数形式)
- 日期/数字格式化(Intl API)
三、SSR和ISR的区别
问题:SSR 和 ISR 的区别是什么?
这是Next.js相关的经典问题,考察你对服务端渲染模式的理解深度。
javascript
// 1. SSR (Server-Side Rendering)
// 每次请求都在服务端渲染
export async function getServerSideProps(context) {
const data = await fetchData()
return { props: { data } }
}
// 特点:
// - 实时性最好,每次请求都是最新数据
// - 服务端压力大
// - TTFB(首字节时间)较长
// - 适合:个性化内容、实时数据
// 2. ISR (Incremental Static Regeneration)
// 静态生成 + 按需更新
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60, // 每60秒重新生成
}
}
// 特点:
// - 结合了SSG和SSR的优点
// - 首次访问返回缓存,后台触发重新生成
// - 后续访问看到新版本
// - 适合:博客、商品详情页(更新不频繁)
// 3. 核心区别对比
| 维度 | SSR | ISR |
|------|-----|-----|
| 渲染时机 | 每次请求 | 构建时 + 按时间增量 |
| 数据实时性 | 实时 | 有延迟(取决于revalidate) |
| 服务端压力 | 高 | 低 |
| 缓存策略 | 不可缓存 | 可缓存 |
| SEO | 好 | 好 |
| 首屏性能 | 中等 | 好 |
// 4. 实际应用场景
// 新闻网站:首页用ISR(每分钟更新),详情页用ISR
// 电商网站:商品页用ISR,购物车用CSR
// 管理后台:全部用CSR
// 5. 补充:SSG (Static Site Generation)
// 构建时生成一次,适合不变化的内容
export async function getStaticProps() {
const data = await fetchData()
return { props: { data } } // 没有revalidate
}
四、怎么优化性能的
问题:项目中怎么优化性能的?
性能优化是面试必问题,需要从多个维度展开,结合项目实际。
javascript
// 1. 加载性能优化
// 1.1 代码分割
// React.lazy + Suspense
const HomePage = React.lazy(() => import('./pages/HomePage'))
// 路由级别分割
<Route path="/home" component={React.lazy(() => import('./pages/Home'))} />
// 1.2 图片优化
// - 使用WebP格式
// - 懒加载(Intersection Observer)
// - 响应式图片(srcset)
// 1.3 资源预加载
<link rel="preload" href="/fonts/xx.woff2" as="font" />
<link rel="prefetch" href="/next-page.js" />
// 2. 运行时性能优化
// 2.1 避免不必要的重渲染
// React: React.memo, useMemo, useCallback
const MemoizedComponent = React.memo(MyComponent)
function Parent() {
const memoizedValue = useMemo(() => compute(a, b), [a, b])
const memoizedCallback = useCallback(() => doSomething(), [])
return <Child value={memoizedValue} callback={memoizedCallback} />
}
// 2.2 虚拟列表
import { FixedSizeList } from 'react-window'
const VirtualList = ({ items }) => (
<FixedSizeList
height={500}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index]}</div>
)}
</FixedSizeList>
)
// 3. 网络优化
// 3.1 请求合并
// 将多个请求合并成一个
const fetchMultiple = async (ids) => {
const results = await Promise.all(ids.map(id => fetch(`/api/${id}`)))
return results
}
// 3.2 数据缓存
// SWR / React Query
import useSWR from 'swr'
function Profile() {
const { data } = useSWR('/api/user', fetcher, {
revalidateOnFocus: false,
staleTime: 5000 // 5秒内不重新请求
})
}
// 4. 首屏优化指标
- FCP (First Contentful Paint)
- LCP (Largest Contentful Paint)
- TTI (Time to Interactive)
- CLS (Cumulative Layout Shift)
// 5. 监控工具
- Lighthouse
- Web Vitals
- Performance API
五、项目中前端怎么和后端交互的
问题:项目中前端怎么和后端交互的?
考察你对API设计的理解,以及实际项目中的实践。
javascript
// 1. 交互方式
// 1.1 RESTful API(最常见)
// GET /api/users - 获取列表
// GET /api/users/1 - 获取单个
// POST /api/users - 创建
// PUT /api/users/1 - 更新
// DELETE /api/users/1 - 删除
// 1.2 GraphQL
query {
user(id: 1) {
name
email
posts {
title
}
}
}
// 1.3 SSE (Server-Sent Events)
// 服务端推送
const eventSource = new EventSource('/api/events')
eventSource.onmessage = (e) => {
console.log('收到消息:', e.data)
}
// 1.4 WebSocket
// 双向通信
const ws = new WebSocket('ws://localhost:8080')
ws.onmessage = (e) => {
console.log('收到消息:', e.data)
}
// 2. API层封装
// services/user.ts
import request from './request'
export const userApi = {
getList: (params) => request.get('/users', { params }),
getDetail: (id) => request.get(`/users/${id}`),
create: (data) => request.post('/users', data),
update: (id, data) => request.put(`/users/${id}`, data),
delete: (id) => request.delete(`/users/${id}`),
}
// 3. 请求拦截
// request.ts
axios.interceptors.request.use(
config => {
// 添加token
config.headers.Authorization = `Bearer ${getToken()}`
// 添加loading
showLoading()
return config
},
error => Promise.reject(error)
)
// 4. 响应拦截
axios.interceptors.response.use(
response => {
hideLoading()
// 统一处理响应格式
if (response.data.code === 200) {
return response.data.data
}
// 错误处理
return Promise.reject(response.data.message)
},
error => {
hideLoading()
// 401处理
if (error.response?.status === 401) {
redirectToLogin()
}
return Promise.reject(error)
}
)
// 5. 错误处理
try {
const data = await userApi.getList()
} catch (error) {
// 统一错误提示
message.error(error.message || '请求失败')
// 上报错误
reportError(error)
}
六、SSE和WebSocket的区别
问题:SSE和WebSocket的区别,为什么不用WebSocket?
javascript
// 1. SSE (Server-Sent Events)
// - 单向:服务端→客户端
// - 基于HTTP协议
// - 自动重连
// - 只能传文本
// - 浏览器限制(最多6个连接)
// 服务端
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
})
setInterval(() => {
res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`)
}, 1000)
})
// 客户端
const source = new EventSource('/events')
source.onmessage = (e) => {
console.log(JSON.parse(e.data))
}
// 2. WebSocket
// - 双向通信
// - 独立的协议(ws/wss)
// - 需要握手升级
// - 支持二进制
// - 没有连接数限制
// 客户端
const ws = new WebSocket('ws://localhost:8080')
ws.onopen = () => ws.send('hello')
ws.onmessage = (e) => console.log(e.data)
// 3. 为什么不用WebSocket?
// - 需求单向:如果只需要服务端推送,SSE更简单
// - 实现复杂度:SSE基于HTTP,无需额外协议
// - 自动重连:SSE内置,WebSocket需要自己实现
// - 兼容性:现代浏览器都支持SSE
// - 资源开销:WebSocket保持长连接开销更大
// 4. 适用场景对比
| 场景 | 推荐 | 原因 |
|------|------|------|
| 实时通知 | SSE | 单向推送足够 |
| 聊天室 | WebSocket | 需要双向 |
| 股票行情 | SSE | 服务端推送 |
| 游戏 | WebSocket | 低延迟双向 |
| 日志推送 | SSE | 简单可靠 |
// 5. 如果必须用WebSocket的场景
// - 双向通信
// - 高频率交互
// - 二进制数据传输
七、SSE断线重连
问题:SSE断线重连怎么处理?
javascript
// 1. SSE原生支持自动重连
const source = new EventSource('/api/events')
// 默认会尝试重连,间隔约3秒
// 可以通过响应头控制
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Retry-After': '5000' // 5秒后重连
})
// 2. 监听连接状态
source.onopen = (e) => {
console.log('连接成功')
}
source.onerror = (e) => {
console.log('连接错误,尝试重连中...')
if (source.readyState === EventSource.CLOSED) {
console.log('连接已关闭')
}
}
// 3. 自定义重连逻辑
class SSEWithReconnect {
constructor(url, options = {}) {
this.url = url
this.options = options
this.reconnectInterval = options.reconnectInterval || 3000
this.maxRetries = options.maxRetries || Infinity
this.retryCount = 0
this.connect()
}
connect() {
this.source = new EventSource(this.url)
this.source.onopen = (e) => {
console.log('连接成功')
this.retryCount = 0 // 重置重试计数
this.options.onOpen?.(e)
}
this.source.onerror = (e) => {
console.log('连接错误')
this.source.close()
if (this.retryCount < this.maxRetries) {
this.retryCount++
setTimeout(() => {
console.log(`第${this.retryCount}次重连...`)
this.connect()
}, this.reconnectInterval)
} else {
console.log('重连次数已达上限')
this.options.onMaxRetries?.()
}
}
this.source.onmessage = (e) => {
this.options.onMessage?.(e)
}
}
close() {
this.source?.close()
}
}
// 使用
const sse = new SSEWithReconnect('/api/events', {
onMessage: (e) => console.log(e.data),
onOpen: () => console.log('已连接'),
maxRetries: 5,
reconnectInterval: 2000
})
// 4. 心跳检测(保持连接)
// 服务端定期发送注释行保持连接
setInterval(() => {
res.write(': heartbeat\n\n') // 注释行,客户端会忽略
}, 15000)
// 5. 断线原因排查
// - 网络波动
// - 服务端超时
// - 浏览器限制(后台标签页)
// - 代理服务器限制
八、后端RAG和Agent的流程
问题:后端RAG和Agent的流程了解吗?
这是跨界问题,考察你对AI应用的理解,以及技术广度。
javascript
// 1. RAG (Retrieval-Augmented Generation) 流程
// 1.1 离线阶段(索引构建)
文档 -> 分块(Chunking) -> 向量化(Embedding) -> 存储到向量数据库
// 1.2 在线阶段(查询)
用户问题 -> 向量化 -> 向量检索(相似度计算) ->
召回相关文档 -> 组合Prompt -> 调用LLM -> 生成回答
// 2. Agent流程
用户输入 -> LLM规划(ReAct) -> 调用工具 -> 观察结果 ->
再次规划 -> 直到完成任务 -> 返回结果
// 3. 代码示例(简化)
class RAGSystem {
constructor() {
this.vectorDB = new VectorDB()
this.llm = new LLM()
this.embedder = new Embedder()
}
async query(question) {
// 1. 向量化问题
const questionVector = await this.embedder.embed(question)
// 2. 检索相关文档
const relevantDocs = await this.vectorDB.similaritySearch(
questionVector,
limit = 5
)
// 3. 构建Prompt
const context = relevantDocs.join('\n')
const prompt = `
基于以下上下文回答问题:
上下文:${context}
问题:${question}
回答:
`
// 4. 生成回答
const answer = await this.llm.generate(prompt)
return { answer, sources: relevantDocs }
}
}
// 4. Agent示例
class Agent {
async run(task) {
const tools = {
search: async (q) => {/* 搜索 */},
calculator: async (expr) => {/* 计算 */}
}
while (!task.done) {
// 1. 思考下一步
const thought = await this.llm.think(task)
// 2. 执行动作
const result = await tools[thought.tool](thought.input)
// 3. 观察结果
task.observations.push(result)
}
return task.result
}
}
九、RAG中怎么计算chunk相似度的
问题:RAG中怎么计算chunk相似度的?
javascript
// 1. 向量相似度计算方法
// 1.1 余弦相似度 (Cosine Similarity)
function cosineSimilarity(vecA, vecB) {
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0)
const normA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0))
const normB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0))
return dotProduct / (normA * normB)
}
// 结果范围:-1到1,越接近1越相似
// 1.2 欧氏距离 (Euclidean Distance)
function euclideanDistance(vecA, vecB) {
return Math.sqrt(
vecA.reduce((sum, a, i) => sum + Math.pow(a - vecB[i], 2), 0)
)
}
// 值越小越相似
// 1.3 点积 (Dot Product)
function dotProduct(vecA, vecB) {
return vecA.reduce((sum, a, i) => sum + a * vecB[i], 0)
}
// 值越大越相似(需要向量已归一化)
// 2. 实际应用
class VectorDB {
async similaritySearch(queryVector, limit = 5) {
const results = []
for (const doc of this.documents) {
const similarity = cosineSimilarity(queryVector, doc.vector)
results.push({ ...doc, similarity })
}
// 按相似度排序,取前limit个
return results
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit)
}
}
// 3. 优化策略
// - 使用HNSW、IVF等索引加速检索
// - 分片存储
// - 缓存热门查询结果
// - 设置相似度阈值(如 > 0.7)
// 4. 混合检索
// 结合关键词检索(BM25)和向量检索
class HybridSearch {
async search(query) {
const keywordResults = await this.bm25Search(query)
const vectorResults = await this.vectorSearch(query)
// 加权融合
return this.reciprocalRankFusion([
keywordResults,
vectorResults
])
}
}
十、RAG效果不好怎么优化
问题:RAG效果不好怎么优化,如果AI胡言乱语怎么办?
javascript
// 1. RAG效果不好常见原因及优化
// 1.1 分块策略优化
// 原分块:固定500字符
// 优化后:语义分块
function semanticChunking(text) {
// 按段落分块
const paragraphs = text.split('\n\n')
// 合并小段落
return paragraphs.reduce((chunks, p) => {
const lastChunk = chunks[chunks.length - 1]
if (lastChunk && lastChunk.length + p.length < 1000) {
lastChunk += '\n\n' + p
} else {
chunks.push(p)
}
return chunks
}, [])
}
// 1.2 检索优化
// - 增加召回数量(5 → 10)
// - 调整相似度阈值
// - 添加重排序(Re-ranking)
// 1.3 Prompt优化
// 不好的Prompt
const badPrompt = `基于上下文回答问题:${context} 问题:${question}`
// 优化后的Prompt
const optimizedPrompt = `
你是一个专业的AI助手。请严格基于以下提供的上下文回答问题。
如果上下文中没有相关信息,请直接说"抱歉,我无法从提供的资料中找到答案",不要编造。
上下文:
${context}
问题:${question}
请一步步思考:
1. 上下文中有哪些相关信息?
2. 如何基于这些信息回答问题?
3. 最终答案:
回答:
`
// 2. 处理AI胡言乱语
// 2.1 添加安全护栏
function validateAnswer(answer, context) {
// 检查是否包含上下文外的信息
const outOfContext = checkOutOfContext(answer, context)
// 检查是否不确定的表达
const uncertainty = ['可能', '也许', '大概']
const hasUncertainty = uncertainty.some(u => answer.includes(u))
return {
isValid: !outOfContext && !hasUncertainty,
answer: outOfContext ? '无法从资料中找到准确答案' : answer
}
}
// 2.2 设置置信度阈值
if (similarityScore < 0.7) {
return {
answer: '抱歉,这个问题超出了我的知识范围',
confidence: 'low'
}
}
// 2.3 提供来源引用
return {
answer: '根据资料显示...',
sources: ['doc1.pdf', 'doc2.md'],
confidence: 0.95
}
// 3. 完整优化方案
class OptimizedRAG {
async query(question) {
// 1. 多路召回
const chunks = await Promise.all([
this.vectorSearch(question),
this.keywordSearch(question),
this.hydeSearch(question) // 假设性文档嵌入
])
// 2. 重排序
const reranked = await this.reranker.rerank(chunks.flat())
// 3. 动态选择上下文长度
const context = this.selectContext(reranked, question)
// 4. 生成带约束的回答
const answer = await this.llm.generateWithConstraints(
question,
context,
{
noHallucination: true,
maxTokens: 500
}
)
// 5. 后处理验证
return this.validateAndFormat(answer, context)
}
}
十一、树形目录编辑组件
问题:项目中树形目录编辑组件拖拉拽效果怎么实现的?每个item的数据结构是什么样的?
javascript
// 1. 数据结构设计
interface TreeNode {
id: string // 唯一标识
name: string // 节点名称
type: 'file' | 'folder' // 节点类型
children?: TreeNode[] // 子节点(文件夹特有)
parentId: string | null // 父节点ID
path: string // 路径(用于快速定位)
metadata: { // 元数据
createdAt: Date
updatedAt: Date
size?: number // 文件大小
}
permissions: { // 权限
canEdit: boolean
canDelete: boolean
canMove: boolean
}
}
// 示例数据
const treeData: TreeNode = {
id: 'root',
name: '项目',
type: 'folder',
parentId: null,
path: '/',
children: [
{
id: '1',
name: 'src',
type: 'folder',
parentId: 'root',
path: '/src',
children: [
{
id: '1-1',
name: 'index.ts',
type: 'file',
parentId: '1',
path: '/src/index.ts',
metadata: { size: 1024 }
}
]
}
]
}
// 2. 拖拉拽实现
// 2.1 使用react-dnd
import { useDrag, useDrop } from 'react-dnd'
const DraggableTreeNode = ({ node, onDrop }) => {
// 拖拽逻辑
const [{ isDragging }, drag] = useDrag(() => ({
type: 'TREE_NODE',
item: { node },
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
canDrag: () => node.permissions.canMove
}))
// 放置逻辑
const [{ isOver }, drop] = useDrop(() => ({
accept: 'TREE_NODE',
drop: (item) => {
if (item.node.id !== node.id) {
onDrop(item.node, node) // 源节点,目标节点
}
},
canDrop: (item) => {
// 不能拖放到自己或自己的子节点
return node.type === 'folder' &&
!isDescendant(item.node, node)
},
collect: (monitor) => ({
isOver: monitor.isOver()
})
}))
return (
<div
ref={(node) => drag(drop(node))}
style={{ opacity: isDragging ? 0.5 : 1 }}
>
{node.name}
</div>
)
}
// 2.2 排序算法
function reorderTree(tree, sourceId, targetId, position) {
// 找到源节点和目标节点
const sourceNode = findNode(tree, sourceId)
const targetNode = findNode(tree, targetId)
// 移除源节点
removeNode(tree, sourceId)
// 插入到新位置
insertNode(tree, targetId, sourceNode, position)
return { ...tree }
}
// 2.3 更新路径
function updatePaths(node, parentPath = '') {
node.path = parentPath + '/' + node.name
if (node.children) {
node.children.forEach(child =>
updatePaths(child, node.path)
)
}
}
// 3. 性能优化
- 虚拟滚动(大量节点时)
- 防抖处理(拖拽过程中)
- 局部更新(只重新渲染变化的节点)
- 扁平化存储 + 索引(快速查找)
// 4. 撤销/重做
class TreeHistory {
constructor() {
this.undoStack = []
this.redoStack = []
}
push(state) {
this.undoStack.push(state)
this.redoStack = [] // 清空重做
}
undo() {
if (this.undoStack.length > 1) {
const current = this.undoStack.pop()
this.redoStack.push(current)
return this.undoStack[this.undoStack.length - 1]
}
}
}
十二、线程和进程的区别
问题:线程和进程的区别?
javascript
// 1. 核心区别
// 1.1 资源占用
// 进程:独立的内存空间、文件描述符、环境变量
// 线程:共享进程的资源,只有独立的栈和寄存器
// 1.2 通信方式
// 进程:IPC(管道、消息队列、共享内存、Socket)
// 线程:直接读写共享内存
// 1.3 开销
// 进程:创建、切换开销大
// 线程:创建、切换开销小
// 1.4 稳定性
// 进程:一个进程崩溃不影响其他进程
// 线程:一个线程崩溃可能影响整个进程
// 2. 类比理解
// 进程 = 工厂(独立厂房、独立资源)
// 线程 = 车间工人(共享厂房、共享资源)
// 3. 多进程 vs 多线程
| 维度 | 多进程 | 多线程 |
|------|--------|--------|
| 资源开销 | 高 | 低 |
| 数据共享 | 复杂(需IPC) | 简单(共享内存) |
| 安全性 | 高(隔离) | 低(易相互影响) |
| 编程难度 | 简单 | 复杂(需处理竞态) |
| 适用场景 | 隔离性要求高 | 高性能计算 |
// 4. JavaScript中的线程
// - 主线程:UI渲染、事件处理
// - Web Worker:独立线程(不能操作DOM)
// - Service Worker:网络代理
十三、浏览器的线程进程
问题:浏览器的线程进程有哪些?
javascript
// 1. 浏览器进程架构
// 1.1 主进程 (Browser Process)
// - 管理各个标签页
// - 地址栏、书签、前进/后退
// - 网络请求
// - 文件访问
// 1.2 渲染进程 (Renderer Process) - 每个标签页一个
// - GUI渲染线程(解析HTML/CSS,布局绘制)
// - JS引擎线程(执行JS代码,V8引擎)
// - 事件触发线程(管理事件队列)
// - 定时器线程(setTimeout/setInterval)
// - 网络线程(XHR/fetch请求)
// - 合成线程(页面合成)
// 2. 进程间通信 (IPC)
// 主进程 ←→ 渲染进程 ←→ GPU进程
// ←→ 插件进程
// ←→ 网络进程
// 3. 渲染进程中的线程关系
// - JS引擎线程和GUI渲染线程互斥
// (JS执行时会阻塞渲染)
// - 事件触发线程管理事件队列
// - 定时器线程独立计时
// 4. 示例:页面加载时的线程工作
// 1) 主进程请求资源
// 2) 网络线程下载资源
// 3) 渲染线程解析HTML/CSS
// 4) JS引擎线程执行脚本
// 5) 合成线程合成页面
// 6) GPU进程绘制页面
// 5. 性能优化角度
- 减少JS长时间执行(避免阻塞渲染)
- 使用Web Worker处理耗时任务
- 关键资源预加载
- 避免强制同步布局
十四、浏览器不同tab的通信
问题:浏览器不同tab是怎么通信的?
javascript
// 1. 同源策略下的通信方式
// 1.1 localStorage(最常用)
// Tab A
localStorage.setItem('key', 'value')
localStorage.setItem('event', JSON.stringify({
type: 'update',
data: {}
}))
// Tab B
window.addEventListener('storage', (e) => {
if (e.key === 'event') {
const { type, data } = JSON.parse(e.newValue)
// 处理事件
}
})
// 1.2 BroadcastChannel
// Tab A
const channel = new BroadcastChannel('tab_channel')
channel.postMessage({ type: 'update', data: {} })
// Tab B
const channel = new BroadcastChannel('tab_channel')
channel.onmessage = (e) => {
console.log('收到消息:', e.data)
}
// 1.3 SharedWorker
// shared-worker.js
const connections = []
onconnect = (e) => {
const port = e.ports[0]
connections.push(port)
port.onmessage = (e) => {
// 广播给所有连接
connections.forEach(p => p.postMessage(e.data))
}
}
// Tab A
const worker = new SharedWorker('shared-worker.js')
worker.port.postMessage('hello')
// Tab B
const worker = new SharedWorker('shared-worker.js')
worker.port.onmessage = (e) => {
console.log('收到:', e.data)
}
// 2. 跨域情况
// 2.1 postMessage
// Tab A (http://a.com)
const popup = window.open('http://b.com/page')
popup.postMessage('hello', 'http://b.com')
// Tab B (http://b.com)
window.addEventListener('message', (e) => {
if (e.origin === 'http://a.com') {
console.log('收到:', e.data)
e.source.postMessage('world', e.origin)
}
})
// 3. 通信方式对比
| 方式 | 同源 | 跨域 | 数据大小 | 复杂度 |
|------|------|------|----------|--------|
| localStorage | ✅ | ❌ | 5MB | 简单 |
| BroadcastChannel | ✅ | ❌ | 不限 | 简单 |
| SharedWorker | ✅ | ❌ | 不限 | 中等 |
| postMessage | ✅ | ✅ | 不限 | 中等 |
| WebSocket | ✅ | ✅ | 不限 | 复杂 |
// 4. 实际应用场景
- 登录状态同步
- 主题切换同步
- 数据更新通知
- 协同编辑
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| 项目架构 | 分层设计、技术选型、数据流向 |
| i18n | 懒加载、命名空间、SSR适配 |
| SSR vs ISR | 渲染时机、数据实时性、缓存策略 |
| 性能优化 | 代码分割、虚拟列表、请求合并、缓存 |
| SSE vs WebSocket | 单向/双向、协议、自动重连 |
| SSE断线重连 | 自动重连、自定义重试、心跳 |
| RAG流程 | 分块→向量化→检索→生成 |
| chunk相似度 | 余弦相似度、欧氏距离、点积 |
| RAG优化 | 分块策略、Prompt优化、防幻觉 |
| 树形组件 | 递归结构、拖拉拽、撤销重做 |
| 进程线程 | 资源占用、通信、开销 |
| 浏览器进程 | 主进程、渲染进程、线程分工 |
| Tab通信 | localStorage、BroadcastChannel、postMessage |
📌 最后一句:
Bilibili这场实习生面试,虽然只有30分钟,但信息密度很高。实习生的竞争力,不在于会多少框架API,而在于能把一个点挖多深,能把相关的知识串多远。