面经-04

3 阅读32分钟

一、实现垂直水平居中的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/finallyqueueMicrotaskMutationObserverawait 之后的续写代码。
  • 宏任务(task/mactask) → 下一轮
    • setTimeout / setInterval / setImmediate(Edge/Node)messageChannelI/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)之后,会去清空微任务,再执行下一个宏任务
几个容易踩的坑
  1. setTimeout(fn, 0) 不是立刻执行:它一定在下一轮;且浏览器会有最小延迟(嵌套多了通常 ≥4ms)。
  2. 微任务太多会阻塞渲染:清空微任务发生在渲染之前;如果持续往微任务队列塞任务,可能长时间不渲染。
  3. async/await 也会“跳出”当前调用栈await 后面的代码永远晚于本轮同步。
  4. 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),逐一比较新旧依赖:
      • 如果所有依赖都没变 → 不执行回调。
      • 如果有任何依赖变了 → 执行回调,并更新记录的依赖数组。
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. 核心差异

特性Vue3React
响应式原理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 + CSSOMRender 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)

  • 定义:当元素的外观(颜色、背景、阴影等)改变,但不影响几何属性时,触发重绘。

  • 触发条件

    • 修改 colorbackground-colorvisibility 等属性。

👉 重绘只涉及样式和像素绘制,相对比回流开销小。

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 的属性/方法offsetTopoffsetHeightscrollTopgetBoundingClientRect()

    • 这些属性会强制浏览器立即计算布局。
  • 解决方法:先读再写,不要交替读写。

    // ❌ 交替读写 → 多次回流
    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;
  • 动画优化:尽量使用 transformopacity 来做动画,因为它们只触发 合成,不会触发回流/重绘。


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 的依赖条件

  1. 必须使用 ES Module (ESM)

    • import/export 是静态结构,编译时可以分析依赖关系
    • CommonJS (require/module.exports) 是运行时动态引入,无法静态分析。
  2. 无副作用(Side Effects)

    • 代码不会在执行时产生额外的影响
    • 比如:
    import './style.css'  // 有副作用(引入样式)
    
    • package.json 里声明:
    {
      "sideEffects": false
    }
    

    表示所有文件都无副作用,可以安全移除。

  3. 压缩工具的支持

    • 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`,整个类依然会被打包。

五、优化建议

  1. 使用EMS(import/export),少用require
  2. package.json"sideEffects": false 或精确标记:
{
  "sideEffects": ["./src/polyfill.js", "*.css"]
}
  1. 开启生产模式
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)。
  • 路由守卫 / 异步组件加载 / 滚动行为 / 动态路由匹配 都是常见功能点。

十.解释浏览器渲染原理及关键渲染路径优化

渲染流程高层次
  1. HTML解析 → 构建DOM树
  2. CSS解析 → 构建CSSOM树(样式树)
  3. 构建渲染树(render tree)也就是DOM和CSS结合
  4. 布局(Reflow): 计算每个节点的几何信息
  5. 绘制(Paint): 将布局后的节点绘制到图层
  6. 合成(Composite): 将图层合成到屏幕
关键渲染路径(Critical Rendering Path)优化要点
  • 减少阻塞资源:将关键的CSS放在head (内联关键css); 将不必要的 CSS/JS 延迟或异步加载(deferasync)。
  • 避免阻塞解析的脚本:使用 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.构建速度优化(提升开发体验)

  1. 合理使用mode

    • 开发环境:mode: "development",开启更快的增量编译。
    • 生产环境:mode: "production",自动开启优化(如压缩、Tree-Shaking)
  2. 缩小构建范围

    • includeexclude:在 babel-loaderts-loader 等只处理 src 目录。
    • 使用 resolve.modules 限制查找范围,减少路径解析开销。
  3. 缓存(Cache)

    • babel-loader:启用 cacheDirectory: true
    • Webpack 5 内置 持久化缓存cache: { type: 'filesystem' }
  4. 多进程/多实例并行

    • thread-loader:多进程处理 JS/TS 转译。
    • terser-webpack-plugin:配置 parallel: true 开启多线程压缩。
  5. 增量编译

    • 使用 webpack-dev-server + HMR(热更新)。
    • webpack 5 内置更高效的 持久化缓存 + HMR
  6. 跳过不必要的解析

    • noParse: 对不依赖其他模块的库(如 jquery)设置 noParse
    • externals: 将 reactvue 等大库从构建中排除,走 CDN。

二、打包体积优化(减少最终 bundle 大小)

  1. Tree Shaking

    • 确保使用 ES Module (import/export)。
    • 生产模式默认启用,删除无用代码。
  2. 代码分割(Code Splitting)

    • SplitChunksPlugin:提取第三方依赖、公共代码。
    • 动态 import():实现路由懒加载、组件懒加载。
  3. 压缩优化

    • TerserWebpackPlugin:压缩 JS。
    • css-minimizer-webpack-plugin:压缩 CSS。
    • image-webpack-loader 或 Webpack 5 内置的 asset:优化图片。
  4. 减少 polyfill

    • @babel/preset-env + useBuiltIns: "usage":只按需引入 polyfill。
    • core-js:避免全量引入。
  5. 按需引入

    • babel-plugin-import(Ant Design 等 UI 库)。
    • lodash-webpack-plugin(优化 lodash)。
    • 使用 dayjs 替代 moment.js(减少体积)。
  6. 资源优化

    • 图片:小图用 asset/inline 转成 base64,大图用 asset/resource
    • 字体/图标:用 SVG Sprite 或 Iconfont 替代。

三、构建体验优化

  1. 分析工具

    • webpack-bundle-analyzer:分析 bundle 体积。
    • 找出体积最大的包,优化依赖。
  2. DllPlugin / HardSourceWebpackPlugin(Webpack4)

    • 提前编译不变的依赖,加速二次构建。
  3. 模块联邦(Module Federation,Webpack 5)

    • 多项目共享依赖,减少重复打包。

⚡ 常见组合方案:

  • 开发环境HMR + cache + thread-loader
  • 生产环境Tree-Shaking + SplitChunks + 压缩 + 按需加载 + 图片优化

目标:缩短构建时间、减小产物体积、提高运行时性能。

构建(dev)速度优化

  • 开启持久化缓存 cache: { type: 'filesystem' }
  • 开启多线程 loader(thread-loader)或 babel-loadercacheDirectory
  • 使用 esbuild-loader / swc-loader 替代 Babel(显著加速)。
  • 减少入口和模块解析范围(配置 resolve.modulesextensions、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 找到大包并优化。

注意:优化要有度,优先解决用户可感知的慢(首屏 / 交互),再对构建性能做微调。

十二.如何处理大文件分片上传?

  • 目标:支持断点续传、上传大文件(避免一次性上传失败)、提升并发与体验。
  • 常见流程
  1. 前端将文件按固定 chunkSize 切分为块(Blob.slice)。
  2. 生成每个块的校验(例如 MD5/sha1),并向后端查询哪些块已上传(实现断点续传)。
  3. 并行上传若干块(限制并发数,例如 3-5)。
  4. 所有块上传成功后,通知后端合并块(服务器拼接或云存储 API 支持分片合并)。
  5. 前端显示进度、支持暂停/恢复、失败重试(指数退避)。

示例要点(伪代码)

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
  • 记录三次快照:
    1. 初始页面加载后(baseline)。
    2. 执行常见操作 / 路由切换 5~10 次
    3. 返回初始状态
  • 对比对象数量和retain size ,重点看:
    • Window 数量是否增加
    • EventListener 是否持续增加
    • Detached HTMLDivElement 或其他 DOM 节点是否堆积
    • 自定义对象(比如Vue组件实例,Store)是否能够回收
(2) 查找保留引用(Retainers)
  • 在Heap Snapshot中,右键点一个异常对象(比如 Window / https://xxxEventListener)。
  • 查看Retainers(保留者),找到持有它的是谁
    • 常见问题:
      • 全局变量/闭包 引用了旧的对象
      • 定时器(setTimeOut/setInterval)未清理
      • 事件监听器未解绑
      • Vue/React 组件销毁后,依然有引用。
(3) EventListener 检查
  • Elements → Event Listeners 面板查看 DOM 节点绑定的事件。

  • 如果页面操作多次后,事件数量不断增加 → 没有解绑。

  • 常见问题:

    • window.addEventListener 没有在组件销毁时 removeEventListener
    • document.addEventListener 绑定全局事件没清理。

👉 解决办法:

  • Vue 3 用 onMounted / onUnmounted 管理事件。
  • React 用 useEffect(() => {... return () => {...}}) 解绑。
(4) 定时器/异步引用
  • 检查 setIntervalsetTimeout,在组件销毁时是否清理。

  • 检查 Promisefetch 等异步请求是否可能持有组件引用(比如 .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 项目常见内存泄漏主要来源:

  1. 事件监听未解绑onUnmounted 清理。
  2. 定时器未清理clearInterval / clearTimeout
  3. 闭包/全局引用 DOM → 置空引用。
  4. watch 未停止 → 调用 stop()
  5. 第三方库实例未销毁 → 调用 .destroy() / .dispose()
  6. 路由 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-ifv-forv-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. 核心差异总结

对比点ReactVue
定位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 对比 oldChildrennewChildren 时,优先依据 key 匹配节点;相同 key 且类型相同则复用并更新 props,否则销毁旧节点并创建新节点。
  • 好的 key 能减少创建/销毁次数,保持组件实例和 state 的稳定性,从而避免不必要的重渲染或状态错位。

实践建议

  • 使用稳定唯一的 id(后端 id、业务唯一 id)作为 key。
  • 尽量避免在动态列表(有增删改或排序)中使用索引作为 key。

十七. 讲下 React Hooks 和 Class 组件相比,解决了什么问题? 它的实现原理是什么?

Hooks 解决的问题

  1. 逻辑复用更简单:以前 Class 常用 HOC/Render Props 来复用逻辑,但会产生嵌套/抽象层次复杂。Hooks 通过自定义 Hook(函数)可以更自然地组合和复用逻辑。
  2. 副作用和 state 更聚合:Class 的生命周期方法会把相关逻辑拆散到 componentDidMount/componentDidUpdate/componentWillUnmount 等,Hooks(useEffectuseState)允许把相关的 state 与副作用放在同一代码块,便于维护。
  3. 消除 this 问题:函数组件没有 this,避免了 this 绑定错误和类构造的复杂性。
  4. 更好的代码组织:多个 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 分析产物:antdmoment、几个图表库和一些共享工具占用大量体积。
  • 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' }),减少增量构建时间。
  • 运行时优化

    • 在 React 里避免不必要的重渲染:使用 React.memouseMemouseCallback 在必要处缓存组件/计算。
    • 对长列表使用虚拟列表(react-window / react-virtualized)。
  • 监控与回归验证

    • 部署后用 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、长任务、网络阻塞等),逐项解决(拆包、按需、懒加载、压缩、缓存、主线程卸载),最后验证效果并监控回归。
  • 性能优化是持续过程,需结合监控、自动化构建与最小化第三方影响来长期维护。