一、实现垂直水平居中的5种方案
1. Flex 布局
.parent {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
height: 100vh; /* 示例高度 */
}
优点:简洁、语义清晰,现代布局首选。
缺点:IE9- 不支持。
2. Grid 布局
.parent{
display:grid;
place-items:center;
height:100vh;
}
优点:语义非常简洁,一行代码搞定。
缺点:老旧浏览器支持较差。
3. 绝对定位 + transform
.parent {
position: relative;
height: 100vh;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
优点:兼容性非常好(IE9+)。
缺点:需要额外写 transform
。
4. 绝对定位 + margin auto
.parent {
position: relative;
height: 100vh;
}
.child {
position: absolute;
top: 0; bottom: 0;
left: 0; right: 0;
margin: auto;
width: 200px; /* 必须指定宽高 */
height: 100px;
}
优点:CSS2 就能用,兼容性极好。
缺点:必须固定宽高,不适合自适应内容。
5. Table-cell + vertical-align
.parent {
display: table;
width: 100%;
height: 100vh;
}
.child {
display: table-cell;
text-align: center; /* 水平居中 */
vertical-align: middle; /* 垂直居中 */
}
.inner {
display: inline-block; /* 避免子元素100%占满 */
}
优点:IE8- 也能支持。
缺点:需要多一层包装,写法稍繁琐。
📌 总结推荐:
- 现代项目 → Flex / Grid。
- 需要兼容性 → transform 方案。
- 固定宽高元素 → margin auto。
- 老项目/兼容 IE8 → table-cell。
二、事件循环机制:setTimeout、Promise、async/await 执行顺序
1、三条铁律(先记住结论)
- 同步代码→立即执行
- 微任务(microtask)→ 在本轮脚本 或 任务结束后,立即清空
Promise.then/catch/finally
、queueMicrotask
、MutationObserver
、await
之后的续写代码。
- 宏任务(task/mactask) → 下一轮
setTimeout / setInterval / setImmediate(Edge/Node)
、messageChannel
、I/O
、事件回调等。
浏览器一轮循环的大致顺序:
执行一个宏任务 → 清空所有微任务 → (可能)渲染 → 取下一个宏任务 → 清空微任务 → 渲染 …
2、API 对应的队列
API | 队列 | 何时执行 |
---|---|---|
Promise.then() | 微任务 | 当前宏任务结束后,立刻 |
async/await | 微任务 | await 之后的代码是微任务 |
queueMicrotask() | 微任务 | 同上 |
setTimeout(fn, 0) | 宏任务 | 下一轮(最小延迟也可能被浏览器夹到≥4ms) |
3、基础例子(浏览器)
- 例1:Promise 优先于 setTimeout
console.log('A');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('then'));
console.log('B');
// A → B → then → timeout
// 同步AB先跑,本轮结束清空微任务(then);再到下一轮宏任务(setTimeout)
- 例2:微任务会在下一次宏任务之前清空
Promise.resolve().then(() => {
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
});
console.log(4);
// 4 → 1 → 3 → 2
// 4是同步任务;清空微任务.then先打印1;并把一个微任务(3)和一个宏任务(2)排队;继续清空微任务打印3;最后下一轮才有(2)
- 例3:
async/await
本质是 Promise;await
之后的代码是微任务
async function f() {
console.log('f start');
await null; // 等价于 await Promise.resolve(null)
console.log('f end'); // 微任务 !!!非常关键
}
console.log('script start');
f();
console.log('script end');
//**输出**:script start → f start → script end → f end
- 例4:多次await 的顺序(微任务清空到为“空”未知)
async function f() {
console.log(1);
await 0;
console.log(2);
await 0;
console.log(3);
}
console.log(0);
f();
console.log(4);
// **输出**:0 → 1 → 4 → 2 → 3
// 解释:本轮末清空微任务时,会把 `2` 和随后产生的继续微任务(打印 `3`)**在同一轮**依次执行到队列清空。
- 例5 :计时器回调里产生的微任务,会先于下一个计时器
setTimeout(() => {
console.log('t1');
Promise.resolve().then(() => console.log('micro in t1'));
}, 0);
setTimeout(() => console.log('t2'), 0);
// **输出**:t1 → micro in t1 → t2
// 执行完一个宏任务(t1)之后,会去清空微任务,再执行下一个宏任务
几个容易踩的坑
setTimeout(fn, 0)
不是立刻执行:它一定在下一轮;且浏览器会有最小延迟(嵌套多了通常 ≥4ms)。- 微任务太多会阻塞渲染:清空微任务发生在渲染之前;如果持续往微任务队列塞任务,可能长时间不渲染。
async/await
也会“跳出”当前调用栈:await
后面的代码永远晚于本轮同步。- 在
keep-alive
或长驻组件里不断产生微任务/定时器却不释放,容易造成卡顿或泄漏(结合你项目要注意onUnmounted
/onDeactivated
清理)。
三、React Hooks 的依赖数组作用原理
1. 依赖数组是什么?(尤其是 useEffect/useMemo/useCallback这些Hook)
useEffect(() => {
console.log("副作用逻辑");
}, [count, user]);
// [count, user] 就是依赖数组
它告诉 React:
- 只有依赖变化时,才需要重新执行 Hook 的回调。
- 如果数组为空
[]
,表示只在组件 挂载(mount) 和 卸载(unmount) 时执行一次。-
关于挂载和卸载执行一次的解释:
useEffect(()=>{ console.log("副作用逻辑"); return ()=>{ console.log('清理逻辑') } },[]) // 挂载 → 执行副作用函数 1 次 // 卸载 → 执行清理函数 1 次(如果有返回值)
- 教材里说「一次」只是强调副作用逻辑不会随着更新反复执行
-
2. 原理机制(核心:依赖比较)
- React Hooks 内部维护了一套Hook调用顺序表(Hook index → Hook state),依赖数组就存储在这个表里
- 执行流程简易版:
1. 首次渲染
- React 记录依赖数组
[count, user]
。 - 执行副作用回调
effect()
。 2. 更新渲染 - React 再次渲染组件时,会取到新的依赖数组
[count, user]
。 - React 用浅比较(Object.is),逐一比较新旧依赖:
- 如果所有依赖都没变 → 不执行回调。
- 如果有任何依赖变了 → 执行回调,并更新记录的依赖数组。
- React 记录依赖数组
3. 举例说明
没有依赖数组
useEffect(() => {
console.log("每次渲染都执行");
});
👉 每次渲染都会执行,因为 React 没有依赖对比的依据。
空依赖数组 []
useEffect(() => {
console.log("只在挂载时执行");
}, []);
👉 只执行一次(挂载),卸载时清理函数会被调用。
指定依赖
useEffect(() => {
console.log("count 改变才执行");
}, [count]);
👉 只有 count
值变化时才执行副作用。
4. useMemo 和 useCallback
依赖数组在这两个Hook的作用是缓存结果
- useMemo(factory,deps)
- 当依赖没有变的时候返回上次计算的结果
- 避免重复计算
const expensiveValue = useMemo(() => {
return slowFn(count);
}, [count]);
- useCallback(fn,deps)
- 当依赖没有改变,返回同一个函数的引用
- 避免子组件因为“props” 变化而不必要的重复渲染
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
// 本质上 useCallback(fn, deps) 等价于:
// useMemo(() => fn, deps)
5. 为什么要正确写依赖?
React 在 严格模式 和 eslint-plugin-react-hooks 插件里,会强制要求你把回调中用到的变量都写进依赖数组。 如果少写:
- React 可能用到过期的变量(闭包陷阱)。
如果多写:
- 副作用可能频繁执行(性能问题)。
6. 关键点总结
- 依赖数组控制副作用/计算结果的重新执行时机。
- React 内部通过浅比较(object.is)判断依赖是否变化。
[]
等于 只执行一次;没有依赖数组 等于 每次渲染都执行- useMemo / useCallback 用依赖数组来决定是否复用缓存
- 写依赖数组时要小心“闭包陷阱”,一般遵循 ESLint 的提示即可。
四.Vue3 双向绑定原理及与 React 的差异
1. Vue3 的双向绑定原理
-
Vue3 使用Proxy + 响应式系统来实现数据的双向绑定。核心在reactive() 和 ref()
- 响应式数据:
- Vue3 用 Proxy 代理对象的 get/set,在get时进行依赖收集,在set时触发依赖更新
import { reactive, effect } from "vue" const state = reactive({ count: 0 }) effect(() => { console.log("count 改变时触发:", state.count) }) state.count++ // 触发 effect
- v-model 的实现:
v-model
本质上是props + emit
的语法糖:
<!-- 子组件 --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> defineProps(['modelValue']) defineEmits(['update:modelValue']) </script>
<!-- 父组件 --> <Child v-model="username" />
等价于:
<Child :modelValue="username" @update:modelValue="username = $event" />
- 响应式数据:
👉 所以 Vue3 的双向绑定是 响应式系统(数据劫持)+ v-model 语法糖(事件驱动) 的组合。
2. React的数据流特点
- React 没有双向绑定,只有 单向数据流:
- props:数据从父组件传给子组件;
- state:组件内部维护状态;
- 受控组件:表单值由
state
管理,事件驱动更新。
function InputExample() {
const [value, setValue] = useState("");
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
// 这其实就是 React 里“模拟” v-model 的方式:手动绑定 `value` + `onChange`。
3. 核心差异
特性 | Vue3 | React |
---|---|---|
响应式原理 | Proxy 劫持对象属性,依赖收集 + 派发更新 | 无响应式,依赖 setState / useState 触发重新渲染 |
数据流 | 双向绑定 (v-model 简化父子通信) | 单向数据流,手动管理受控组件 |
更新机制 | 精细化依赖追踪(修改哪个属性就更新对应 effect) | 粗粒度重新渲染(调用 setState 会触发组件重新渲染) |
写法体验 | 表单场景更简洁,直接 v-model | 需要 value + onChange 手动绑定 |
五.实现带缓存机制的斐波那契数列函数
斐波那契数列(Fibonacci Sequence)是一个非常经典的数列:
👉 定义
- 第 0 项 = 0
- 第 1 项 = 1
- 从第 2 项开始,每一项等于前两项之和: F(n)=F(n−1)+F(n−2)
function createFib() {
const cache = {};
function fib(n) {
if (n <= 1) return n;
if (cache[n]) return cache[n]; // 直接返回缓存结果
cache[n] = fib(n - 1) + fib(n - 2); // 递归并存缓存
return cache[n];
}
return fib;
}
const fib = createFib();
console.log(fib(10)); // 55
console.log(fib(50)); // 12586269025
六.浏览器渲染原理与重绘(Repaint)/回流(Reflow)优化
一、浏览器渲染原理
浏览器从接收到 HTML/CSS/JS 到页面展示,大致流程是:
1. 解析 HTML → 构建 DOM 树
- HTML 标签被解析成节点对象(DOM)。
2. 解析 CSS → 构建 CSSOM 树
- CSS 规则被解析成对象模型(CSSOM)。
3. 合并 DOM + CSSOM → Render Tree(渲染树)
- 只包含可见节点及其样式信息(比如
display: none
的不会进入渲染树)。
4. 布局(Layout / Reflow)
- 计算每个节点的几何信息(位置、大小等)。
5. 绘制(Paint / Repaint)
- 将渲染树中的节点转换为实际像素(颜色、文字、阴影等)。
6. 分层(Layering) & 合成(Compositing)
- 有些元素(如 3D transform、动画、视频、position: fixed 等)会单独分层,GPU 合成后送到屏幕显示。
二、重绘(Repaint)与回流(Reflow)
1. 回流(Reflow / Layout)
-
定义:当元素的几何属性(大小、位置)发生变化时,需要重新计算布局。
-
触发条件:
- 添加/删除 DOM 节点
- 元素尺寸改变(width、height、padding、margin、border…)
- 页面初始化渲染
- 改变字体大小
- 滚动、调整窗口大小 👉 回流开销比重绘大,因为要重新计算布局、可能导致整个页面重新渲染。
2. 重绘(Repaint / Paint)
-
定义:当元素的外观(颜色、背景、阴影等)改变,但不影响几何属性时,触发重绘。
-
触发条件:
- 修改
color
、background-color
、visibility
等属性。
- 修改
👉 重绘只涉及样式和像素绘制,相对比回流开销小。
3. 区别
- 回流必定会引发重绘;
- 重绘不一定会引发回流。
三、优化策略
1. 避免频繁操作 DOM
-
合并 DOM 操作:批量修改样式,避免一条一条修改。
// ❌ 不好 el.style.width = "100px"; el.style.height = "200px"; el.style.color = "red"; // ✅ 好(一次性修改) el.style.cssText = "width: 100px; height: 200px; color: red;";
-
使用文档片段(DocumentFragment)
在内存中批量构建 DOM,再一次性插入。
2. 避免强制同步布局(Layout Thrashing)
-
触发 reflow 的属性/方法:
offsetTop
、offsetHeight
、scrollTop
、getBoundingClientRect()
- 这些属性会强制浏览器立即计算布局。
-
解决方法:先读再写,不要交替读写。
// ❌ 交替读写 → 多次回流 div.style.width = div.offsetWidth + 1 + "px"; div.style.height = div.offsetHeight + 1 + "px"; // ✅ 先读后写 → 一次回流 const w = div.offsetWidth; const h = div.offsetHeight; div.style.width = w + 1 + "px"; div.style.height = h + 1 + "px";
3. 使用 CSS3 合成层(GPU 加速)
-
独立图层减少回流范围:
transform: translateZ(0);
will-change: transform, opacity;
-
动画优化:尽量使用
transform
和opacity
来做动画,因为它们只触发 合成,不会触发回流/重绘。
4. 减少复杂选择器和深层嵌套
- 浏览器渲染时是 从右到左 解析选择器,过度复杂的选择器会降低性能。
5. 批量更新 DOM
- 使用 requestAnimationFrame 做动画更新,避免在一帧中多次触发重绘。
- 事件委托,减少绑定过多事件处理器。
四、总结口诀
- 能避免回流就避免回流,用
transform/opacity
代替top/left/width/height
。 - 避免频繁 DOM 读写交替,读写分离。
- 批量操作 DOM,用
fragment
或一次性修改样式。 - 利用合成层,减少回流影响范围。
七.Webpack 的 Tree Shaking 实现原理
一、什么是 Tree Shaking
- “摇树”,去掉无用代码
- 目标:在打包时,只保留真正用到的导出,删除没用到的。
// utils.js
export function add(a, b) { return a + b }
export function sub(a, b) { return a - b }
// index.js
import { add } from './utils'
console.log(add(1, 2))
// 打包的时候只保留add,sub会被摇掉
二、Tree Shaking 的依赖条件
-
必须使用 ES Module (ESM)
- import/export 是静态结构,编译时可以分析依赖关系
- CommonJS (
require/module.exports
) 是运行时动态引入,无法静态分析。
-
无副作用(Side Effects)
- 代码不会在执行时产生额外的影响
- 比如:
import './style.css' // 有副作用(引入样式)
- 在
package.json
里声明:
{ "sideEffects": false }
表示所有文件都无副作用,可以安全移除。
-
压缩工具的支持
- Webpack 本身不会删除代码,只是标记哪些代码没用到
- 需要配合
Terser
(生产环境默认启用)来真正删除。
三、Webpack Tree Shaking 的实现原理
Webpack 主要分三步:
1.ESM 静态分析
- Webpack 在解析
import/export
时,能提前知道依赖关系和哪些导出未使用。
2. 标记未使用代码
-
Webpack 在打包产物中,会保留整个模块,但对没用到的导出打上注释:
// utils.js function add(a, b) { return a + b } /* unused harmony export sub */ function sub(a, b) { return a - b }
3. Terser 删除死代码
- 生产环境构建时,Terser 进行 DCE(Dead Code Elimination),真正把
sub
代码删除
四、Tree Shaking 的局限性
1.动态导入无法摇掉
const utils = require('./utils')
console.log(utils.add(1,2))
//因为 `utils` 里可能用到全部导出。
2.副作用代码保留
export const foo = 1;
console.log('hello'); // 不能删除
类的方法不会被摇掉
class Test {
method1() {}
method2() {}
}
export default Test
// 即使只用到 `method1`,整个类依然会被打包。
五、优化建议
- 使用EMS(import/export),少用require
- 在
package.json
加"sideEffects": false
或精确标记:
{
"sideEffects": ["./src/polyfill.js", "*.css"]
}
- 开启生产模式
webpack --mode production
六、总结
- Webpack Tree Shaking = 静态分析 ESM → 标记无用导出 → 压缩工具删除死代码
- 关键点:ESM + sideEffects 配置 + Terser
- 限制:动态引入、类方法、带副作用代码不容易摇掉。
八.实现 Promise.all 的异常处理机制
-
Promise.ALL → 全部成功才成功;其中只要有一个失败就失败
-
目标:当并行多个异步任务的时候,如何有控制地处理某些失败而不直接全部短路,或实现“尽量全部完成并返回错误信息”的语义。【也就是用Promise.all 实现 Promise.allSettled 】
-
常见策略
- 原生
Promise.all
(短路):任一 Promise reject,则整体 reject(默认行为)。 Promise.allSettled
:等待全部完成,返回每个的状态与结果/原因。- 自己包装每个 Promise,使其永远 resolve,返回
{status, value}
结构(兼容老环境)。 - 失败重试 / 限流 / 超时包装。
- 原生
-
例子:allSettled 风格(兼容
Promise.all
行为但不短路)
function allWithStatus(promises) {
return Promise.all(promises.map(p =>
Promise.resolve(p)
.then(value => ({ status: 'fulfilled', value }))
.catch(reason => ({ status: 'rejected', reason }))
));
}
// 使用
allWithStatus([p1, p2, p3]).then(results => {
// results 中包含每个 promise 的状态和值/原因
});
九.如何实现前端路由?Hash和History模式的区别
基本原理
- 路由:通过拦截URL变化并渲染对应的组件(不刷新页面)
- 常见两种实现方式:
- Hash模式(#/path): 监听hash change 或者读取 location.hash,不触发服务器请求,并且兼容性好
- History模式(pushState): 使用History.pushState / replaceState + popstate,URL更干净需要后端配合重定向到同一入口
区别对比
- URL 可读性:History 更好(无
#
)。 - 服务器支持:History 需要后端将任意匹配路由重定向到 SPA 的 index.html;Hash 不需要。
- SEO:传统上 History 更友好(但现代 SSR/Prerender 更常用)。
- 兼容性:Hash 在非常老的浏览器也能工作;History 要求支持 HTML5 history API(IE10+)。
实现要点(简化版)
// Hash Router 简化
window.addEventListener('hashchange', onRoute);
function onRoute() {
const path = location.hash.slice(1) || '/';
renderRoute(path);
}
// History Router 简化
window.addEventListener('popstate', onRoute);
function navigate(path) {
history.pushState({}, '', path);
renderRoute(path);
}
注意
- History 模式必须处理子资源直达(刷新时服务器返回 index.html)。
- 路由守卫 / 异步组件加载 / 滚动行为 / 动态路由匹配 都是常见功能点。
十.解释浏览器渲染原理及关键渲染路径优化
渲染流程高层次
- HTML解析 → 构建DOM树
- CSS解析 → 构建CSSOM树(样式树)
- 构建渲染树(render tree)也就是DOM和CSS结合
- 布局(Reflow): 计算每个节点的几何信息
- 绘制(Paint): 将布局后的节点绘制到图层
- 合成(Composite): 将图层合成到屏幕
关键渲染路径(Critical Rendering Path)优化要点
- 减少阻塞资源:将关键的CSS放在head (内联关键css); 将不必要的 CSS/JS 延迟或异步加载(
defer
、async
)。 - 避免阻塞解析的脚本:使用
defer
(保持执行顺序)或async
(不保证顺序)。、
<script defer src="xx.js">
- **下载异步**:不会阻塞 HTML 解析,和 HTML 解析并行下载。
- **执行顺序保证**:等 HTML 解析完毕(`DOMContentLoaded` 之前),再按 HTML 中的书写顺序执行。
- 所以 **保持执行顺序**。
<script async src="xx.js">
- **下载异步**:不会阻塞 HTML 解析。
- **执行顺序不保证**:哪个脚本先下载完,就先执行。
- 所以可能会打乱顺序。
-
减少重排(reflow)和重绘(repaint) :批量 DOM 操作、使用
transform
/opacity
做动画(触发合成层而非重排)。 -
按需渲染 / 代码分割(SSR / CSR 混合) :优先渲染首屏内容(Critical CSS / SSR / HTML 结合)。
-
资源优化:压缩、压缩图片、使用现代图片格式(WebP/AVIF)、使用 CDN、HTTP/2 或 HTTP/3。
-
使用浏览器缓存策略:合理设置 Cache-Control、ETag,利用 service worker 做离线缓存或资源预缓存。
-
预加载 / 预取:
<link rel="preload">
(关键资源),rel="prefetch"
(未来导航资源)。
十一.Webpack构建优化有哪些具体措施?
1.构建速度优化(提升开发体验)
-
合理使用mode
- 开发环境:
mode: "development"
,开启更快的增量编译。 - 生产环境:
mode: "production"
,自动开启优化(如压缩、Tree-Shaking)
- 开发环境:
-
缩小构建范围
include
和exclude
:在babel-loader
、ts-loader
等只处理src
目录。- 使用
resolve.modules
限制查找范围,减少路径解析开销。
-
缓存(Cache)
babel-loader
:启用cacheDirectory: true
。- Webpack 5 内置 持久化缓存:
cache: { type: 'filesystem' }
。
-
多进程/多实例并行
thread-loader
:多进程处理 JS/TS 转译。terser-webpack-plugin
:配置parallel: true
开启多线程压缩。
-
增量编译
- 使用
webpack-dev-server
+ HMR(热更新)。 webpack 5
内置更高效的 持久化缓存 + HMR。
- 使用
-
跳过不必要的解析
noParse
: 对不依赖其他模块的库(如jquery
)设置noParse
。externals
: 将react
、vue
等大库从构建中排除,走 CDN。
二、打包体积优化(减少最终 bundle 大小)
-
Tree Shaking
- 确保使用 ES Module (
import/export
)。 - 生产模式默认启用,删除无用代码。
- 确保使用 ES Module (
-
代码分割(Code Splitting)
SplitChunksPlugin
:提取第三方依赖、公共代码。- 动态
import()
:实现路由懒加载、组件懒加载。
-
压缩优化
TerserWebpackPlugin
:压缩 JS。css-minimizer-webpack-plugin
:压缩 CSS。image-webpack-loader
或 Webpack 5 内置的asset
:优化图片。
-
减少 polyfill
@babel/preset-env
+useBuiltIns: "usage"
:只按需引入 polyfill。core-js
:避免全量引入。
-
按需引入
babel-plugin-import
(Ant Design 等 UI 库)。lodash-webpack-plugin
(优化 lodash)。- 使用
dayjs
替代moment.js
(减少体积)。
-
资源优化
- 图片:小图用
asset/inline
转成 base64,大图用asset/resource
。 - 字体/图标:用 SVG Sprite 或 Iconfont 替代。
- 图片:小图用
三、构建体验优化
-
分析工具
webpack-bundle-analyzer
:分析 bundle 体积。- 找出体积最大的包,优化依赖。
-
DllPlugin / HardSourceWebpackPlugin(Webpack4)
- 提前编译不变的依赖,加速二次构建。
-
模块联邦(Module Federation,Webpack 5)
- 多项目共享依赖,减少重复打包。
⚡ 常见组合方案:
- 开发环境:
HMR + cache + thread-loader
。 - 生产环境:
Tree-Shaking + SplitChunks + 压缩 + 按需加载 + 图片优化
。
目标:缩短构建时间、减小产物体积、提高运行时性能。
构建(dev)速度优化
- 开启持久化缓存
cache: { type: 'filesystem' }
。 - 开启多线程 loader(
thread-loader
)或babel-loader
的cacheDirectory
。 - 使用
esbuild-loader
/swc-loader
替代 Babel(显著加速)。 - 减少入口和模块解析范围(配置
resolve.modules
、extensions
、alias)。 - 使用
DllPlugin
(老方案)或 Module Federation 进行依赖隔离。
产物体积优化(prod)
- Tree shaking:确保使用 ES modules,开启
mode: 'production'
。 - 代码分割:
splitChunks
,按路由懒加载,vendor 与 common 分离。 - 压缩:Terser(JS),cssnano(CSS),并开启压缩选项(移除 console / 开发代码)。
- 图片/字体压缩并做 Resource hints。
- 长缓存策略:文件名带 contenthash。
- 剔除 polyfills 或按需引入(
core-js
按需)。 - 分析包体:
webpack-bundle-analyzer
找到大包并优化。
注意:优化要有度,优先解决用户可感知的慢(首屏 / 交互),再对构建性能做微调。
十二.如何处理大文件分片上传?
- 目标:支持断点续传、上传大文件(避免一次性上传失败)、提升并发与体验。
- 常见流程
- 前端将文件按固定
chunkSize
切分为块(Blob.slice)。 - 生成每个块的校验(例如 MD5/sha1),并向后端查询哪些块已上传(实现断点续传)。
- 并行上传若干块(限制并发数,例如 3-5)。
- 所有块上传成功后,通知后端合并块(服务器拼接或云存储 API 支持分片合并)。
- 前端显示进度、支持暂停/恢复、失败重试(指数退避)。
示例要点(伪代码)
const chunkSize = 5*1024*1024;
const chunks = sliceFile(file, chunkSize);
const uploaded = await checkUploadedChunks(fileId); // 后端返回已存在块索引
// 并发上传
await PromisePool({ items: chunksToUpload, concurrency: 3, task: uploadChunk });
await notifyMerge(fileId, totalChunks);
十三.高并发场景下的前端性能优化策略
- 关注点:降低对后端压力、提高前端并发抗压能力、确保用户体验。
策略清单
- 限流/节流:对触发高频请求的操作(搜索、滚动加载、表格筛选)加节流/防抖。
- 本地与边缘缓存:使用浏览器缓存、Service Worker、CDN 缓存静态资源与部分 API 响应。
- 降采样/采样上报:在高并发情况下减少埋点/日志上报频率,采样上报。
- 图像与媒体按需加载:延迟加载、占位图、webp/avif。
- 客户端合并请求:对小频请求做合并(batching),或使用 GraphQL 的 persisted queries。
- 后端熔断与退化策略:前端在后端限流/熔断时给出合理降级(静态缓存、提示“功能暂不可用”)。
- WebSocket / SSE 优化:对于高频实时推送,使用 push 通道替代轮询,服务端做消息合并。
- 资源预热 / 连接复用:HTTP/2 或 HTTP/3,长连接复用,减少握手开销。
- 客户端压力测试:用工具模拟大流量场景(前端结合后端测试),发现瓶颈并补救。
十四.前端灰度发布实施方案
目标:实现增量上线(小比例流量→扩大→全部),快速回滚,降低风险。
常用策略
-
基于用户属性的路由(Server-side / Edge) :
- 通过用户 ID、IP、地区、请求头或 cookie 做百分比路由。
- 在 Edge/CDN 或网关做流量分配到不同后端版本(A/B)。
-
前端 Canary / Feature Flag(特性开关) :
- 使用 Feature Flag 平台(如 LaunchDarkly、开源 Unleash)或自建。
- 配置面板实时打开/关闭功能;支持分组、百分比投放、规则发布。
-
客户端灰度:
- 发布前端两个版本(主应用路由到不同静态资源域名或路径),用 CDN / 路由控制分发新静态资源给部分用户。
-
回滚与监控:
- 必须配合实时监控(错误率、响应时延、业务关键指标),快速回滚机制(自动或人工)。
- 自动化的健康检查与熔断策略:当异常率超阈值自动回退流量。
落地建议
- 先用 Feature Flag 做功能级灰度(最灵活、安全)。
- 对于构建发布(代码级)灰度,结合 CDN 路由或网关做流量分配。
- 建立回滚 SOP(日志、追踪链路、应用版本映射)与演练。
十五.内存泄漏排查清单
(1) 多次快照对比
- 打开 Memory → Heap snapshot。
- 记录三次快照:
- 初始页面加载后(baseline)。
- 执行常见操作 / 路由切换 5~10 次。
- 返回初始状态。
- 对比对象数量和retain size ,重点看:
- Window 数量是否增加
- EventListener 是否持续增加
Detached HTMLDivElement
或其他 DOM 节点是否堆积- 自定义对象(比如Vue组件实例,Store)是否能够回收
(2) 查找保留引用(Retainers)
- 在Heap Snapshot中,右键点一个异常对象(比如
Window / https://xxx
或EventListener
)。 - 查看Retainers(保留者),找到持有它的是谁
- 常见问题:
- 全局变量/闭包 引用了旧的对象
- 定时器(setTimeOut/setInterval)未清理
- 事件监听器未解绑
- Vue/React 组件销毁后,依然有引用。
- 常见问题:
(3) EventListener 检查
-
在 Elements → Event Listeners 面板查看 DOM 节点绑定的事件。
-
如果页面操作多次后,事件数量不断增加 → 没有解绑。
-
常见问题:
window.addEventListener
没有在组件销毁时removeEventListener
。document.addEventListener
绑定全局事件没清理。
👉 解决办法:
- Vue 3 用
onMounted
/onUnmounted
管理事件。 - React 用
useEffect(() => {... return () => {...}})
解绑。
(4) 定时器/异步引用
-
检查
setInterval
、setTimeout
,在组件销毁时是否清理。 -
检查
Promise
、fetch
等异步请求是否可能持有组件引用(比如.then
中用到组件数据,但组件销毁了引用还在)。
(5) 路由切换内存回收
-
重点关注
Window
对象:-
你截图中
Window ×70
→ 说明路由切换后老的上下文没释放。 -
可能原因:
- 使用了
iframe
或动态window.open
没销毁。 - 路由切换后 Vue/React 组件引用了全局变量,导致整个上下文挂住。
- 外部库(比如第三方 UI 插件)没正确清理。
- 使用了
-
👉 检查方式:
- 记录页面操作(路由切换 10 次),看
Window
数量。 - 如果每次多一个,说明有上下文泄漏。
(6) Performance 录制验证
- 打开Performance→勾选Memory→录制用户操作过程
Window
增长 → 说明路由切换时上下文泄漏。EventListener
增长 → 很可能事件监听没解绑。
Vue 3 项目里常见内存泄漏场景 + 对应修复代码示例
(1) 事件监听未解绑
❌ 问题代码
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
window.addEventListener('resize', () => {
console.log('window resized')
})
})
</script>
👉 每次进入组件都会新增监听器,但离开时不会清理,EventListener
数量不断增加。
✅ 修复代码
<script setup>
import { onMounted, onUnmounted } from 'vue'
function handleResize() {
console.log('window resized')
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
(2) 定时器未清理
❌ 问题代码
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
setInterval(() => {
console.log('polling...')
}, 1000)
})
</script>
👉 组件销毁后定时器还在执行,持有组件作用域引用。
✅ 修复代码
<script setup>
import { onMounted, onUnmounted } from 'vue'
let timer
onMounted(() => {
timer = setInterval(() => {
console.log('polling...')
}, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
(3) 全局状态/闭包 DOM或者大对象
❌ 问题代码
<script setup>
import { ref, onMounted } from 'vue'
const cache = [] // 全局数组,永远不会清理
onMounted(() => {
const el = document.querySelector('#my-div')
cache.push(el) // el 即使从 DOM 中删除,依然被引用
})
</script>
👉 cache
永远持有 DOM 引用 → 出现 Detached DOM 节点。
✅ 修复代码
<script setup>
import { onMounted, onUnmounted } from 'vue'
let el = null
onMounted(() => {
el = document.querySelector('#my-div')
})
onUnmounted(() => {
el = null // 清理 DOM 引用
})
</script>
(4) watch 未正确停止
❌ 问题代码
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal) => {
console.log('count changed:', newVal)
})
</script>
- 如果组件被销毁,watch仍然可能或者(比如绑定到外部store),导致内存泄漏
✅ 修复代码
<script setup>
import { ref, watch, onUnmounted } from 'vue'
const count = ref(0)
const stop = watch(count, (newVal) => {
console.log('count changed:', newVal)
})
onUnmounted(() => {
stop() // 手动停止 watch
})
</script>
(5) 第三方库未销毁(图表 / 地图 / WebSocket)
❌ 问题代码
<script setup>
import { onMounted } from 'vue'
import * as echarts from 'echarts'
let chart
onMounted(() => {
chart = echarts.init(document.getElementById('chart'))
})
</script>
👉 组件销毁后 ECharts 实例还存在 → 大对象泄漏。
✅ 修复代码
<script setup>
import { onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
let chart
onMounted(() => {
chart = echarts.init(document.getElementById('chart'))
})
onUnmounted(() => {
if (chart) {
chart.dispose() // ✅ 销毁实例
chart = null
}
})
</script>
(6) 路由切换导致组件未销毁
- 在 Vue Router 中,如果你用了
keep-alive
,组件不会被销毁,而是缓存。 - 这可能导致事件监听、定时器、WebSocket 继续存在。
👉 解决办法:
- 在
onDeactivated
/onActivated
中控制资源开关。
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('组件重新激活')
// 重新开启定时器 / WebSocket
})
onDeactivated(() => {
console.log('组件失活')
// 暂停定时器 / WebSocket
})
</script>
✅ 总结
Vue 3 项目常见内存泄漏主要来源:
- 事件监听未解绑 →
onUnmounted
清理。 - 定时器未清理 →
clearInterval
/clearTimeout
。 - 闭包/全局引用 DOM → 置空引用。
- watch 未停止 → 调用
stop()
。 - 第三方库实例未销毁 → 调用
.destroy()
/.dispose()
。 - 路由 keep-alive → 使用
onActivated
/onDeactivated
控制资源释放。
十五.React 和 Vue,说一下这两个框架在设计思路上有什么本质的区别?
1. 设计哲学
-
React:UI = f(state)
React 的核心理念是 函数式编程,把 UI 看成是 状态到视图的映射。- 核心只有 View 层(Virtual DOM + Diff 算法),其它功能(路由、状态管理等)都依赖生态。
- 更像一个 库,而不是框架。
-
Vue:渐进式框架
Vue 从一开始就是 框架化设计,内置模板语法、指令系统、双向绑定等,并配套了 Vue Router、Vuex/Pinia。- 提供 开箱即用的完整解决方案。
- 适合上手快、渐进增强。
2. 视图层表达方式
-
React:JSX + JavaScript 驱动一切
- 使用 JSX(JavaScript 的扩展语法)来描述 UI。
- 模板逻辑和渲染逻辑混合在一起,更贴近 函数式编程。
- “All in JS”,事件绑定、条件渲染、循环都用 JS 来写。
-
Vue:模板 + 指令语法(或 Composition API)
- 模板和逻辑有一定的分离,使用
v-if
、v-for
、v-model
等指令,写法直观。 - Vue3 的
setup()
和 Composition API 接近 React Hooks,但依旧保留模板。 - 更强调 声明式 UI,对初学者友好。
- 模板和逻辑有一定的分离,使用
3. 数据流管理
-
React:单向数据流
- 强调数据只能 自上而下 传递(props)。
- 状态管理推荐 Redux、MobX 或官方的 Context API。
- 逻辑更自由,但复杂度交给开发者管理。
-
Vue:双向绑定 + 单向数据流结合
- 提供
v-model
做 双向绑定,减少表单类代码的样板。 - 父子组件依旧是单向数据流,但通过
v-model
简化了输入交互。 - 状态管理用 Vuex/Pinia,规范统一。
- 提供
4. 框架定位
-
React:库的心态 → 强调灵活性与组合
- 官方团队只管 View 层,其他由社区生态解决。
- 更适合大型复杂项目,但学习曲线偏陡。
-
Vue:框架的心态 → 提供一条龙方案
- 官方团队管控生态(Vue Router、Pinia、Vue CLI/Vite)。
- 更适合中小团队快速开发。
5. 核心差异总结
对比点 | React | Vue |
---|---|---|
定位 | UI 库 | 渐进式框架 |
哲学 | UI = f(state),函数式 | 模板驱动,声明式 |
视图 | JSX | 模板 + 指令 |
数据流 | 单向 | 双向绑定 + 单向流结合 |
上手难度 | 偏高,需要生态 | 上手快,官方全家桶 |
灵活性 | 更高,自由度大 | 相对规范,约束更强 |
👉 本质上:
- React 追求的是 最小内核 + 强大生态,强调函数式思想。
- Vue 追求的是 完整体验 + 渐进增强,强调易用性和工程化。
十六.React 里的 key 有什么用?为什么不能用 index作为 key?把原理说一下。
用途
key
用于标识列表中每个元素的身份,帮助 React 在 Reconciliation(比对更新)阶段把旧的子节点和新的子节点正确匹配,从而决定是复用、移动还是销毁 DOM/组件实例。- 有稳定 key 时,React 可以最小化 DOM 操作并保持组件内部状态(例如 input 的值、组件局部 state)不被错配。
为什么不推荐用 index(数组索引)
- 当列表可能发生增删改或重排序时,index 会随着位置改变而变化,导致 React 错把某个组件实例从一个项目复用到另一个项目,造成状态错位(例如输入框文本跑到别的项上)。
- 只有在列表静态不变、不会插入/删除/重排,或者只是一次性的渲染时,index 才可接受。
底层原理(简化版)
- React 对比
oldChildren
和newChildren
时,优先依据key
匹配节点;相同key
且类型相同则复用并更新 props,否则销毁旧节点并创建新节点。 - 好的
key
能减少创建/销毁次数,保持组件实例和 state 的稳定性,从而避免不必要的重渲染或状态错位。
实践建议
- 使用稳定唯一的 id(后端 id、业务唯一 id)作为 key。
- 尽量避免在动态列表(有增删改或排序)中使用索引作为 key。
十七. 讲下 React Hooks 和 Class 组件相比,解决了什么问题? 它的实现原理是什么?
Hooks 解决的问题
- 逻辑复用更简单:以前 Class 常用 HOC/Render Props 来复用逻辑,但会产生嵌套/抽象层次复杂。Hooks 通过自定义 Hook(函数)可以更自然地组合和复用逻辑。
- 副作用和 state 更聚合:Class 的生命周期方法会把相关逻辑拆散到
componentDidMount
/componentDidUpdate
/componentWillUnmount
等,Hooks(useEffect
、useState
)允许把相关的 state 与副作用放在同一代码块,便于维护。 - 消除 this 问题:函数组件没有
this
,避免了this
绑定错误和类构造的复杂性。 - 更好的代码组织:多个 state 的逻辑可以通过多个
useState
/useReducer
直观分隔,增强可读性。
实现原理(概念性说明)
- React 为每个函数组件维护一个与之对应的 Hook 链表/数组(Slot list),每次渲染时按固定顺序读取/写入 Hook 的内部状态(依赖、state 值、effect 回调等)。
useState
:会在内部创建一个 state 单元并返回[state, setState]
,setState
将触发调度并在下一次渲染时更新相应的 slot。useEffect
:注册 effect 到当前组件的 effect 列表,commit 阶段在 DOM 更新后执行这些副作用,并在返回清理函数时清理上一次 effect。- 关键:Hooks 依赖调用顺序稳定(每次渲染必须以相同顺序调用相同数量的 Hook),React 通过调用顺序索引定位到正确的 slot。
- React 的底层(Fiber)负责调度渲染与副作用执行,支持中断/优先级,使得 Hooks 能配合 Fiber 的调度语义工作(例如并发渲染、优先级更新)。
注意点
- 不能在条件、循环或嵌套函数中有条件地调用 Hook(保证调用顺序稳定)。
- 对于复杂 state,
useReducer
是替代 class 中复杂 state 的更好选择。
十八. 在项目里做过性能优化吗? 举一个具体的例子,从发现问题、定位、到解决的全过程。
下面给出一个典型且可复用的案例(面试中常见):SPA 首页首屏慢 —— 发现 → 定位 → 方案 → 结果(带技术细节)。
1) 发现问题
- 用户反馈首页打开慢;RUM/监控显示首屏 Time to Interactive(TTI)与 First Contentful Paint(FCP)过高。
- 本地复现:首次加载控制台 Network 面板显示主要 bundle 很大(~2.8MB),解析与执行花费时间长,主线程被阻塞导致首屏渲染延迟。
2) 定位问题
- 用
webpack-bundle-analyzer
分析产物:antd
、moment
、几个图表库和一些共享工具占用大量体积。 - Chrome Performance 记录长任务(>50ms),主要集中在 JS 解析与执行阶段。
- Network 面板:所有页面资源为单个较大 bundle,且很多资源并非首屏需要。
- 结论:首屏加载了过多非必要 JS,同步执行阻塞渲染;资源没有按需分割与懒加载。
3) 解决方案(逐项实施)
-
代码分割(Code Splitting)
- 路由懒加载:把非首屏路由用
import()
动态加载(React.lazy + Suspense 或手动 dynamic import)。 - 将第三方大型库提取为独立 chunk(SplitChunksPlugin 配置)。
- 路由懒加载:把非首屏路由用
-
按需引入/替换大库
- 使用
babel-plugin-import
实现 antd 按需加载组件与样式;把moment
替换为dayjs
(体积小且 API 类似)。
- 使用
-
Tree-Shaking 与 ESModule
- 确保第三方库以 ES module 发布或能被 Tree-Shaking,避免引入整库副作用。
-
静态资源优化
- 图片压缩/按需尺寸切图;关键图片使用
preload
或占位图;其余图片使用懒加载(loading="lazy"
或 IntersectionObserver)。 - 字体按需加载,使用
font-display: swap
避免阻塞渲染。
- 图片压缩/按需尺寸切图;关键图片使用
-
HTTP 优化
- 开启 gzip 或 brotli 压缩;合理设置 Cache-Control;利用 CDN 分发静态资源。
- 使用 HTTP/2 或 HTTP/3(如果可用)提升并发请求性能。
-
减少主线程工作
- 把计算密集型逻辑迁移到 Web Worker。
- 优化第三方脚本(延迟/异步加载非关键代码)。
-
构建与压缩
- 使用 Terser 并开启并行压缩,开启持久化缓存(Webpack 5
cache: { type: 'filesystem' }
),减少增量构建时间。
- 使用 Terser 并开启并行压缩,开启持久化缓存(Webpack 5
-
运行时优化
- 在 React 里避免不必要的重渲染:使用
React.memo
、useMemo
、useCallback
在必要处缓存组件/计算。 - 对长列表使用虚拟列表(react-window / react-virtualized)。
- 在 React 里避免不必要的重渲染:使用
-
监控与回归验证
- 部署后用 RUM、Lighthouse、Chrome DevTools 验证改进点(FCP、TTI、全加载体积等指标)。
- 用
webpack-bundle-analyzer
再次确认产物体积变化。
4) 最终结果(示例性指标)
- bundle 总体从 ~2.8MB 降到 ~750KB(gzip 后更小)。
- 首屏 TTI 从 5s 降到 ~1.4s,FCP 明显改善。
- 首次交互所需 JS 执行时间显著下降,用户感知流畅度提升。
- 构建时间通过缓存与多线程优化也从 60s 降到 ~20s(开发迭代更快)。
5) 总结/要点
- 先量化问题(监控/指标),再定位瓶颈(bundle、长任务、网络阻塞等),逐项解决(拆包、按需、懒加载、压缩、缓存、主线程卸载),最后验证效果并监控回归。
- 性能优化是持续过程,需结合监控、自动化构建与最小化第三方影响来长期维护。