# 御码 2026 前端面试真题（非 Node.js）

> 基于原始题库，完整保留非 Node.js 专题的题目和答案。仅修正明确过时的知识点，原始题库保持不变。

## HTML + CSS

### 标签语义化的意义

1. 开发者更容易理解，减少差异化，方便团队开发和维护
2. 机器更容易理解结果（搜索爬虫、方便其他设备解析（读屏幕软件、盲人设备、移动设备）

### 写页面结构应该注意什么

1. 尽可能少的使用没有语义的 div 和 span 元素
2. 块级元素和内联元素的嵌套一定要符合 web 标准，比如内联元素就是不能嵌套块级元素

### HTML5 新特性

1. 新的语义化元素：article 、footer 、header 、nav 、section
2. 表单增强，新的表单控件：calendar 、date 、time 、email 、url 、search
3. 新的 API：音频(用于媒介回放的 video 和 audio 元素)、图形（绘图 canvas 元素）
4. 离线缓存：早期 Application Cache / cache manifest 已废弃，现代项目使用 Service Worker + Cache Storage
5. 本地存储：localStorage 通常跨会话保存，sessionStorage 随当前标签页会话结束而清理；两者都受同源和容量限制
6. 新的 API：实时通讯，设备能力

### CSS3 新特性

1. CSS3实现圆角（border-radius），阴影（box-shadow），

2. 对文字加特效（text-shadow、），线性渐变（gradient），旋转（transform）

3. transform:rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg);//旋转,缩放,定位,倾斜3.

4. 增加了更多的CSS选择器 多背景 rgba
5. 在CSS3中唯一引入的伪元素是::selection.
6. 媒体查询，多栏布局

### 盒子模型

页面渲染时，dom 元素所采用的 **布局模型**。可通过`box-sizing`进行设置。根据计算宽高的区域可分为：

- `content-box` (W3C 标准盒模型)
- `border-box` (IE 盒模型)
- `padding-box`
- `margin-box` (浏览器未实现)

### BFC

**块级格式化上下文**，是一个独立的渲染区域，让处于 BFC 内部的元素与外部的元素相互隔离，使内外元素的定位不会相互影响。

> IE 下为 Layout，可通过 zoom:1 触发

- 触发条件:
  - 根元素
  - `position: absolute/fixed`
  - `display: inline-block / table`
  - `float` 元素
  - `ovevflow` !== `visible`
- 规则:

  - 属于同一个 BFC 的两个相邻 Box 垂直排列
  - 属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
  - BFC 中子元素的 margin box 的左边， 与包含块 (BFC) border box 的左边相接触 (子元素 absolute 除外)
  - BFC 的区域不会与 float 的元素区域重叠
  - 计算 BFC 的高度时，浮动子元素也参与计算 - 文字层不会被浮动层覆盖，环绕于周围

- 应用:
  - 阻止`margin`重叠
  - 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个`div`都位于同一个 BFC 区域之中)
  - 自适应两栏布局
  - 可以阻止元素被浮动元素覆盖

### 选择器优先级

- `!important` > 行内样式 > `#id` > `.class` > `tag` > \* > 继承 > 默认
- 选择器 **从右往左** 解析

### 去除浮动影响，防止父级高度塌陷

- 通过增加尾元素清除浮动
- `:after / <br> : clear: both`
- 创建父级 BFC
- 父级设置高度

### link 与 @import 的区别

- `link`功能较多，可以定义 RSS，定义 Rel 等作用，而`@import`只能用于加载 css
- 当解析到`link`时，页面会同步加载所引的 css，而`@import`所引用的 css 会等到页面加载完才被加载
- `@import`需要 IE5 以上才能使用
- `link`可以使用 js 动态引入，`@import`不行

### CSS 预处理器(Sass/Less/Postcss)

CSS 预处理器的原理: 是将类 CSS 语言通过 **Webpack 编译** 转成浏览器可读的真正 CSS。在这层编译之上，便可以赋予 CSS 更多更强大的功能，常用功能:

- 嵌套
- 变量
- 循环语句
- 条件语句
- 自动前缀
- 单位转换
- mixin 复用

### 单行文本溢出

```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```

### 多行文本溢出

```css
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /*2*/
-webkit-box-orient: vertical;
```



### flex布局

- Flex是Flexible Box的缩写，意为"弹性布局"，用来为盒状模型提供最大的灵活性。

  - 布局的传统解决方案，基于盒状模型，依赖display属性 + position属性 + float属性。它对于那些特殊布局非常不方便，比如，垂直居中就不容易实现。

- 分清主轴和交叉轴

  - flex-direction 为 row 时，主轴是横向的，相反的为 column 时，主轴是纵向的。

- 简单的分为容器属性和元素属性

  - 容器的属性：

    flex-direction：决定主轴的方向（即子item的排列方法）

    flex-wrap：决定换行规则

    flex-flow：flex-direction`和`flex-wrap`的简写形式，默认值为`row nowrap

    justify-content：对其方式，水平主轴对齐方式

    align-items：对齐方式，竖直轴线方向

  - 项目的属性（元素的属性）：

    flex：是flex-grow和flex-shrink、flex-basis的简写，默认值为0 1 auto。

    order属性：定义项目的排列顺序，顺序越小，排列越靠前，默认为0

    flex-grow属性：定义项目的放大比例，即使存在空间，也不会放大

    flex-shrink属性：定义了项目的缩小比例，当空间不足的情况下会等比例的缩小，如果定义个item的flow-shrink为0，则为不缩小

    flex-basis属性：定义了在分配多余的空间，项目占据的空间。

### 圣杯布局

- 圣杯特点简单记为：有头、有尾、包三列，圣杯布局中间有container大容器包裹着左、中、右三列区域
- 左、中、右是独立的三个区域，都处于一个层级

### 双飞翼布局

- 左、中、右是独立的三个区域，中间区域属于最上面的层级



## 移动端

### 基础知识

- 1英寸(inch) = 2.54厘米(cm)

- IPhone 6 的屏幕分辨率为 750 * 1334  设备独立像素为 375 * 667

- 物理像素：由屏幕制造商决定，屏幕生产后无法修改

- css像素：单位是px，它是为 Web 开发者创造的

- 设备独立像素的出现，使得即使在【**高清屏**】下，也可以让元素有正常的尺寸，让代码不受到设备的影响，它是设备厂商根据屏幕特性设置的，无法更改。

- 1个位图像素对应1个物理像素，图片才能得到完美清晰的展示

- pc端视口：默认宽度和浏览器窗口的宽度一致，也被称为初始包含块`document.documentElement.clientWidth`

- 移动端视口:

  - 布局视口：一般是980px左右，布局视口经过压缩后，横向的宽度用css像素表达就不再是375px了，而是980px

  - 视觉视口：用户可见的区域，它的绝对宽度永远和设备屏幕一样宽

  - 理想视口：布局视口宽度 与 屏幕等宽（设备独立像素），靠meta标签实现

    ```html
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    ```

### 适配

#### viewport 适配

- 方法：拿到设计稿之后，设置布局视口宽度为设计稿宽度，然后直接按照设计稿给宽高进行布局即可。
- 一般适用于：计图稿宽度 < 375

#### <span style="color:#ee0b41">rem适配</span>

- 方案一：（百度）
  1. 设置完美视口
  2. <span style="color:#ee0b41">通过js设置根字体大小 = **( 当前设备横向独立像素值 *100) / 设计稿宽度**</span>
  3. <span style="color:#ee0b41">编写样式时，直接以rem为单位，值为：**设计值 / 100** </span>
  4. 增加 JS 代码进行实时适配
- 方法二：（淘宝、搜狐、唯品会）
  1. 设置完美视口
  2. <span style="color:#ee0b41">通过js设置根字体大小  = **当前设备横向独立像素值 / 10** </span>
  3. <span style="color:#ee0b41">编写样式时，直接以rem为单位，值为：**设计值 / (设计稿宽度 / 10)**</span>
  4. 增加 JS 代码进行实时适配

#### vw适配

vw和vh是两个相对单位

- 1vw = 等于布局视口宽度的1%
- 1vh = 等于布局视口高度的1%

#### 1px物理像素边框

高清屏幕下 1px 对应更多的物理像素，所以 1 像素边框看起来比较粗，解决方法如下

#### 方法一

使用媒查询：

```css
@media screen and (-webkit-min-device-pixel-ratio:2){
    #demo{
        border: 0.5px solid black;
    }
}
或
@media screen and (-webkit-min-device-pixel-ratio:2){
    #demo2::after{
        transform:scaleY(0.5);
    }
}
```

#### 方法二

根据dpr扩大布局视口，例如dpr为n则布局视口改为原来的n倍，则元素尺寸均变为原来的n分之一，为了保证元素尺寸比例不变，扩大根字体为原来的n倍，但整个过程中边框一直用px作为单位，不用rem。

1. rem 页面布局

2. 元素的边框设置为 1px

3. 通过 viewport 中的 initial-scale 将布局视口扩大n倍，这样页面元素就比原来缩小了n倍

```js
var viewport = document.querySelector('meta[name=viewport]')
var scale = 1 / window.devicePixelRatio
viewport.setAttribute('content', 'width=device-width,initial-scale=' + scale);
```

4. 重新设置根元素字体

```js
var fontSize = parseInt(document.documentElement.style.fontSize);
document.documentElement.style.fontSize = fontSize * window.devicePixelRatio + 'px'
```

### 移动端事件

* touchstart   元素上触摸开始时触发
* touchmove   元素上触摸移动时触发
* touchend   手指从元素上离开时触发
* touchcancel   触摸被打断时触发

### 移动端中touchstart,touchend,click执行顺序

- touchstart
- touchend
- click，浏览器在 click 后会等待约300ms去判断用户是否有双击行为，如果300ms内没有再一次click，那么就判定这是一次单击行为

### 点击穿透

- touch 事件结束后会默认触发元素的 click 事件

  方法一：阻止默认行为

  方法二：使背后元素不具备click特性，用touchXxxx代替click

  方法三：让背后的元素暂时失去click事件，300毫秒左右再复原，属性`pointer-events: none;`

  方法四：让隐藏的元素延迟300毫秒左右再隐藏



## `JS`基础相关面试真题&答案

> **准备面试时：把每个概念讲清楚“是什么 → 为什么 → 怎么用 → 注意点 → 代码示例”，并用自己参与过的项目举例说明如何选择并在工程中落地（这是打动面试官的关键）。**

> 总结
>
> - 回答结构建议：**定义 -> 本质 -> 示例 -> 常考陷阱 -> 手写/代码实现（若会）**，这样面试官能快速抓住重点。
> - 面试常要求手写：`bind`、`curry`、`new`、`简单 Promise`、深拷贝（带循环引用） 
> - 遇到异步题一定要说清楚微任务 vs 宏任务与环境差异（浏览器 vs Node）；

### `var` / `let` / `const` 的区别

- **作用域**：`var` 函数作用域或全局；`let/const` 块级作用域。
- **提升（hoisting）**：`var` 声明会被提升并初始化为 `undefined`；`let/const` 被提升但处于 **暂时性死区（TDZ）**，在声明前访问会报 ReferenceError。
- **重声明/重赋值**：`var` 支持同一作用域重复声明；`let` 不允许重复声明；`const` 声明必须初始化且不能重新赋值（引用类型可修改内部状态）。
- **绑定到全局对象**：在浏览器的全局作用域中，`var` 会成为 `window` 属性，`let/const` 不会。
- **面试点**：TDZ、作用域链、`const` 与不可变性的误解（`const` 是不可变绑定，不是不可变值）。

示例：

```javascript
console.log(a); // undefined
var a = 1;

console.log(b); // ReferenceError (TDZ)
let b = 2;

const c = {x:1};
c.x = 2; // 合法，改变的是对象内容，不是绑定
```

### 数据类型 与 `typeof` 的陷阱

- JS 基本类型（primitive）：`undefined, null, boolean, number, bigint, string, symbol`。
- 引用类型：`object`（包括 Array, Function, Date, RegExp, Map, Set 等）。
- `typeof` 陷阱：

- - `typeof null === 'object'`（历史遗留）。
  - `typeof [] === 'object'`（数组不是单独的 typeof）。
  - `typeof function(){}` === `'function'`（函数特殊）。
  - `typeof NaN === 'number'`。
  - `typeof 10n === 'bigint'`，`typeof Symbol() === 'symbol'`。

- 更准确的类型判断：

- - 数组：`Array.isArray(x)`。
  - 更一般：`Object.prototype.toString.call(x)`（返回 `[object Type]`）。
  - `Number.isNaN` 更可靠（`isNaN` 会先强制转换）。

示例：

```javascript
typeof null // "object"
Array.isArray([]) // true
Object.prototype.toString.call(/a/) // "[object RegExp]"
Number.isNaN(NaN) // true
isNaN('a') // true  <-- 因为 'a' -> NaN，再判定
```

### 值类型 vs 引用类型

- **值类型（primitive）**：赋值/传参时拷贝值，互不影响（string, number, boolean, null, undefined, symbol, bigint）。
- **引用类型（object）**：赋值/传参时拷贝的是引用（指针），多个引用指向同一对象，修改会反映到所有引用。
- **重要面试点**：JS 始终是按**值传递**（但是引用的值是指向对象的指针），这常被误说为“按引用传递”。

示例：

```javascript
let a = 1;
let b = a;
b = 2; // a 仍是 1

let o1 = {x:1};
let o2 = o1;
o2.x = 2; // o1.x 也变成 2
```

### 深拷贝 vs 浅拷贝（本质与常见实现）

- **本质**：浅拷贝只复制一层属性（引用仍指向原对象的子对象）；深拷贝递归复制所有可复制的子结构（避免共享引用）。
- **常见实现**：

- - 浅拷贝：`Object.assign({}, obj)`、`{...obj}`、`Array.prototype.slice`、`arr.concat()`。
  - 深拷贝（简单）：`JSON.parse(JSON.stringify(obj))`（缺点：丢失函数、`undefined`、`Symbol`、`BigInt`、日期、正则、无法处理循环引用）。
  - 浏览器/Node：`structuredClone(obj)`（支持更多类型，自动处理循环，但并非所有环境都可用）。
  - 手写深拷贝：递归 + `WeakMap` 处理循环引用，并专门处理 `Date`、`RegExp`、`Map`、`Set` 等类型。

- **面试点**：什么时候用浅拷贝（性能） vs 深拷贝（避免共享引用）；JSON 深拷贝的边界条件；循环引用处理。

示例（简化版深拷贝，支持循环引用、Date、RegExp、Map、Set）：

```javascript
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (map.has(obj)) return map.get(obj);

  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (obj instanceof Map) {
    const m = new Map();
    map.set(obj, m);
    for (const [k, v] of obj) m.set(deepClone(k, map), deepClone(v, map));
    return m;
  }
  if (obj instanceof Set) {
    const s = new Set();
    map.set(obj, s);
    for (const v of obj) s.add(deepClone(v, map));
    return s;
  }

  const copy = Array.isArray(obj) ? [] : {};
  map.set(obj, copy);
  Reflect.ownKeys(obj).forEach(key => {
    copy[key] = deepClone(obj[key], map);
  });
  return copy;
}
```

### 数组常用方法背后的面试点

- **遍历/映射**：

- - `forEach`：无返回值，无法中途跳出（除非抛异常）。
  - `map`：返回新数组，不改变原数组。
  - `filter`：返回满足条件的新数组。
  - `reduce`：最通用，可实现 map/filter/sum/groupBy 等。

- **查找**：`find`, `findIndex`, `indexOf`, `includes`（`includes` 使用 SameValueZero，能检测 NaN）。

- **判断**：`some`, `every`（短路）。

- **变更**：`push/pop/shift/unshift`（原地变更），`slice`（不变更），`splice`（变更）。

- **排序**：`sort` 默认按字符串比较；传 compareFn；注意数值排序需 `a-b`；稳定性依引擎/规范而变（现代引擎通常稳定）。

- **面试点**：

  * 用 `reduce` 实现 `map`/`filter`（考察对高阶函数理解）。

  - `sort` 的副作用与 compare 函数实现。
  - `map` 中返回的数组长度/稀疏数组的问题。
  - 时间复杂度（map/filter O(n); sort O(n log n)）。

* 示例（用 `reduce` 实现 `map`）：

```javascript
function mapByReduce(arr, fn) {
  return arr.reduce((acc, cur, idx) => {
    acc.push(fn(cur, idx, arr));
    return acc;
  }, []);
}
```

### 对象遍历方式对比（`for..in`, `Object.keys`, `Reflect` 等）

- `for..in`：遍历对象**可枚举属性**，包括原型链上的可枚举属性（会遍历继承来的属性）；顺序不是严格保证（尽管现代引擎特定顺序）。
- `Object.keys(obj)`：返回自身可枚举属性的数组（不包含原型）。
- `Object.getOwnPropertyNames(obj)`：返回自身属性（包括不可枚举）但不包含 Symbol。
- `Object.getOwnPropertySymbols(obj)`：获取自身 Symbol 属性。
- `Reflect.ownKeys(obj)`：返回自身所有键（string + symbol）。
- `for..of`：用于可迭代对象（Array、Map、Set、字符串等）。
- **面试点**：遍历时是否需要过滤原型属性；Symbol 属性如何遍历；性能差异（`for` 循环 vs `forEach` vs `for..of`）。

示例：

```javascript
for (const k in obj) { /* 包含原型的可枚举属性 */ }
Object.keys(obj).forEach(k => { /* 只包含自身可枚举 */ });
Reflect.ownKeys(obj).forEach(k => { /* 包含 symbol 和不可枚举 */ });
```

### 隐式类型转换的坑

- **规则**：

- - `+` 若一方是字符串则执行字符串拼接；否则数值化（ToNumber）。
  - 关系比较 `<, >` 会先将两边转换为 primitive（可能是 number 或 string）。
  - `==` 会进行复杂的抽象相等比较（见第 8 条）。
  - 对象转原始值：会尝试 `valueOf()`，若返回原始值则用之；否则 `toString()`。

- **典型陷阱**：

- - `'' + 1` -> `'1'`，`1 + '2'` -> `'12'`，`1 - '2'` -> `-1`。
  - `null == undefined` -> `true`，但 `null == 0` -> `false`（null 不与其它值强制转换）。
  - `[] + []` -> `''`，`[] + {}` -> `"[object Object]"`（对象 toString）。
  - `0 == false` -> true，`'0' == false` -> true（`'0'` -> 0）。
  - `isNaN('')` -> false（'' -> 0），`isNaN('a')` -> true。推荐用 `Number.isNaN`。

- **面试点**：能用具体例子说明 `==` 的行为；对象到原始值转换顺序；`+` 的优先字符串拼接或数字转换问题。

示例：

```javascript
'' + 1 // "1"
1 - '2' // -1
null == undefined // true
'0' == false // true
Number.isNaN('a') // false (不会强制转换)
isNaN('a') // true (会先转 NaN)
```

### `==` 和 `===` 的核心考点

- `===`：严格相等，不做类型转换（若类型不同，直接 false）。
- `==`：抽象相等，会在许多情况下进行类型转换：

- - `null == undefined` -> true
  - 如果类型不同，按表规则转换：字符串与数字 -> 字符串转数字；布尔 -> 转数字；对象 -> 转原始值（调用 valueOf / toString）。

- **面试点**：要能列出若干典型 `==` 的例子并解释原因（尤其 null/undefined、布尔与字符串、对象与原始值比较），以及推荐在业务代码中尽量使用 `===`。

### 执行上下文（execution context）和作用域链（scope chain）

- **执行上下文** 包括：变量环境（variable environment）、词法环境（lexical environment，环境记录 + 外部引用）、`this` 绑定和作用域链。
- **创建阶段（creation）**：

- - 为函数/全局创建绑定（变量提升、函数提升、参数对象）。

- **执行阶段（execution）**：执行代码，变量赋值，函数引用分配。
- **作用域链**：当访问一个标识符时，JS 从当前执行上下文的词法环境开始查找，找不到则向上查找直至全局（或 null）。
- **面试点**：区别词法作用域与动态作用域、变量提升的细节、`let/const` 和 TDZ、如何解释闭包是如何访问外层变量的。

示例（展示作用域链）：

```javascript
function a() {
  let x = 1;
  function b() {
    console.log(x); // 通过作用域链拿到 a 的 x
  }
  return b;
}
const f = a();
f(); // 1
```

### JS 中的 `this` 究竟指向谁？

- 四条规则（优先级）：

1. 1. **new**：构造调用（`new Fn()`）——`this` 指向新创建的实例。
   2. **显式绑定**：`call/apply/bind` 显式指定（`bind` 可创建固化 this 的函数）。
   3. **隐式绑定**：通过对象调用（`obj.fn()`）——`this` 指向该对象（注意丢失绑定，比如 `const f = obj.fn; f()`）。
   4. **默认绑定**：非严格模式下全局（浏览器为`window`，严格模式为 `undefined`）。

- **箭头函数**：没有自己的 `this`，`this` 由定义位置的词法环境确定（即继承外部 `this`）。
- **面试点**：`bind` 与 `new` 的组合行为（若使用 `new` 调用 bind 后的函数，`this` 以 `new` 创建的实例为准）；箭头函数不能作为构造器。

示例：

```javascript
function Foo() { this.x = 1; }
const o = {};
Foo.call(o);
console.log(o.x); // 1

const obj = { x: 1, f() { console.log(this.x); } };
obj.f(); // 1
const g = obj.f;
g(); // undefined (strict) 或 window.x
```

### 闭包（closure）到底是什么？如何判断？

- **定义**：闭包是函数与其**创建时的词法环境**（包含被捕获的变量）的组合。换言之，内部函数保留了对外层作用域变量的引用，即使外层函数已返回。
- **如何判断**：若一个函数引用了在它外部定义的变量，并且该函数在外部函数执行完后仍被调用/保留，则构成闭包。
- **面试点**：闭包引起的内存保留（小心大闭包导致内存），如何避免（避免在全局保存大的闭包状态，及时释放引用）；闭包与模块化的关系（实现封装）。

示例：

```javascript
function counter() {
  let n = 0;
  return function() {
    n += 1;
    return n;
  };
}
const c = counter();
c(); // 1  —— 闭包持有 n
```

### 高阶函数和柯里化（应用场景）

- **高阶函数（HOF）**：接收函数或返回函数的函数（如 `map`, `filter`, `reduce`，或 `compose`）。
- **柯里化（currying）**：把多参数函数转换为一系列单参数函数。常用于**部分应用**、配置化函数、函数复用。
- **面试点**：区别柯里化和部分应用（partial application）；高阶函数在中间件、函数式编程、事件处理等场景的应用。

示例（柯里化）：

```javascript
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
}
const add = (a,b,c) => a+b+c;
const cadd = curry(add);
cadd(1)(2)(3); // 6
```

### 节流（throttle） vs 防抖（debounce）

- **防抖（debounce）**：若短时间内多次触发，只在最后一次触发后执行（适合输入搜索、窗口调整完成后再计算）。
- **节流（throttle）**：限定一定时间内只执行一次（适合滚动、resize 等持续事件）。
- **面试点**：实现带 `leading`/`trailing` 选项（首次是否立即执行、最后一次是否执行），以及使用 `requestAnimationFrame` 实现节流以更平滑的动画。
- **实现**：

防抖：

```javascript
function debounce(fn, wait) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}
```

节流（时间戳版）：

```javascript
function throttle(fn, wait) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}
```

### `bind`、`call`、`apply` 的底层原理

- **实现思路**：把函数作为对象的临时属性，然后调用该属性（这样 `this` 指向该对象），完成后删除该属性。`apply` 接收数组参数，`call` 接收参数列表，`bind` 返回新的函数（闭包）并记录要绑定的 `this` 与前置参数。
- **面试点**：`bind` 返回的函数可用于 `new`（有特殊处理：若用 `new` 调用，`this` 指向实例），需要保留原函数的 prototype 链以支持 `instanceof`。

示例（简化实现）：

```javascript
Function.prototype.myCall = function(context, ...args) {
  context = context || globalThis;
  const fnSym = Symbol();
  context[fnSym] = this;
  const result = context[fnSym](...args);
  delete context[fnSym];
  return result;
};

Function.prototype.myBind = function(context, ...args) {
  const self = this;
  function bound(...rest) {
    // 支持 new
    const isNew = this instanceof bound;
    return self.apply(isNew ? this : context, args.concat(rest));
  }
  bound.prototype = Object.create(self.prototype);
  return bound;
};
```

### 立即执行函数（IIFE）是怎么工作的？

- **目的**：创建独立作用域，避免污染全局，常用于早期模块化或块级封装。
- **写法**：

- - `(function(){ /*...*/ })();`
  - `(() => { /*...*/ })();`
  - `!function(){ /*...*/ }();`（有趣但不常用）

- **注意**：前面可能需要分号（防止与前一个未结束的语句造成解析错误）。

### 纯函数、副作用 与 函数式编程入门

- **纯函数**：相同输入总返回相同输出、不会产生副作用（不修改外部变量、无 I/O 等）。
- **副作用**：修改外部状态、网络请求、DOM 操作、控制台输出等。
- **函数式编程（FP） 初识**：强调不可变数据、纯函数、组合（compose）、高阶函数、声明式处理。
- **面试点**：如何把副作用隔离到边缘（side-effect free core），以及用 Immutable/structural sharing 优化。

### 手写 `bind` 实现（更完整版）

```javascript
Function.prototype.myBind = function(context, ...args) {
  if (typeof this !== 'function') throw new TypeError('Bind must be called on function');
  const self = this;
  function bound(...rest) {
    // 如果通过 new 调用，this 会是实例，忽略绑定的 context
    const isNew = this instanceof bound;
    return self.apply(isNew ? this : context, args.concat(rest));
  }
  // 保持原型链以支持 new
  bound.prototype = Object.create(self.prototype);
  return bound;
};
```

### 手写柯里化函数

- 支持一次或多次传参，直到参数数量满足原函数长度：

```javascript
function curry(fn, ...preset) {
  return function curried(...args) {
    const all = [...preset, ...args];
    if (all.length >= fn.length) return fn(...all.slice(0, fn.length));
    return curry(fn, ...all);
  };
}

// 用法
const add = (a,b,c) => a+b+c;
const cAdd = curry(add);
cAdd(1)(2)(3); // 6
cAdd(1,2)(3); // 6
```

### 函数组合、记忆化（memoization）

- **组合（compose/pipe）**：

```javascript
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
```

- **记忆化**：利用 `Map` 缓存计算结果（注意：参数是对象时需要特殊处理或使用 WeakMap）：

```javascript
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const res = fn.apply(this, args);
    cache.set(key, res);
    return res;
  };
}
```

- **面试点**：记忆化适用于纯函数，注意缓存失效/内存泄漏问题以及参数可序列化性。

### `setTimeout` 和 `setInterval` 的陷阱

- `setInterval` 可能导致任务重叠（任务执行时间超过间隔），建议用递归 `setTimeout` 来确保间隔是任务完成后的固定延迟。
- 最小延迟限制（浏览器或 Node 在后台页或某些环境会限制最小延迟）。
- **清除**需要 `clearTimeout/clearInterval`。
- **计时精度**：不保证精确（受事件循环阻塞影响）。
- **示例**（安全的间隔）：

```javascript
function safeInterval(fn, delay) {
  let stopped = false;
  function run() {
    if (stopped) return;
    const p = Promise.resolve().then(fn);
    p.finally(() => setTimeout(run, delay));
  }
  setTimeout(run, delay);
  return () => { stopped = true; };
}
```

### 事件循环（Event Loop）完整解析

- **主要概念**：

- - **调用栈**（call stack）
  - **宏任务队列（macrotasks）**：`setTimeout`, `setInterval`, I/O, UI rendering callbacks
  - **微任务队列（microtasks）**：`Promise.then/catch/finally`, `queueMicrotask`, DOM MutationObserver
  - 执行顺序：执行当前 call stack -> 执行所有微任务直到空 -> 渲染（浏览器）-> 执行一个宏任务 -> 重复。

- **Node vs 浏览器差异**：

- - Node 有 `process.nextTick`（比 microtask 更高优先级），也有宏任务队列细分。

- **面试点**：用例子说明输出顺序（微任务优先于宏任务），并能说明在某个输出序列中为啥 Promise.then 先于 setTimeout。

经典示例：

```javascript
console.log('script start');

setTimeout(() => console.log('timeout'), 0);

Promise.resolve().then(() => {
  console.log('promise1');
}).then(() => {
  console.log('promise2');
});

console.log('script end');
// 输出： script start, script end, promise1, promise2, timeout
```

### `Promise` 基本语法 + 错误捕获

- `.then(onFulfilled, onRejected)`：推荐链式 `.then(...).catch(...)`；`catch` 等于 `.then(null, onRejected)`。
- **错误传播**：`.then` 内抛错会变成返回的 Promise 的 reject，可被后续的 `.catch` 捕获。
- **finally**：无论成功或失败都会执行；返回值会被忽视（但可用于清理）。
- **面试点**：错误冒泡链、在 `.then` 内返回 Promise 会等待它 resolve/reject。

示例：

```javascript
Promise.resolve(1)
  .then(x => x+1)
  .then(x => { throw new Error('err') })
  .catch(err => console.error('caught', err));
```

### `async` / `await` 的底层运行机制

- `async` 函数总是返回 `Promise`。
- `await` 会暂停 async 函数的执行（在当前执行帧结束后，`await` 后的代码会作为微任务恢复执行），等待表达式解析（若不是 Promise，会被 `Promise.resolve()` 包装）。
- **底层等效**：用 Generator + Promise 来实现（Babel 转译思路）。
- **面试点**：`await` 只会暂停当前 async 函数，不会阻塞主线程；错误可被 `try/catch` 捕获。

伪转译（概念）：

```javascript
async function foo() {
  const a = await p1();
  const b = await p2();
  return a + b;
}
// 大致像
function foo() {
  return new Promise((resolve, reject) => {
    p1().then(a => {
      p2().then(b => resolve(a+b), reject);
    }, reject);
  });
}
```

### `Promise.all`、`allSettled`、`race`、`any` 用法对比

- `Promise.all(iterable)`：全部成功 -> 返回结果数组；任意一个失败 -> 立刻 reject（返回第一个遇到的 reject）。
- `Promise.allSettled(iterable)`：等待全部完成（不论成功失败），返回每项 `{status, value/reason}`。
- `Promise.race(iterable)`：返回第一个完成（resolve 或 reject）的 Promise 的结果。
- `Promise.any(iterable)`（ES2021）：返回第一个 resolve 的值；若全部 reject，则 reject 一个 `AggregateError`（包含所有错误）。
- **面试点**：选择哪个取决于业务需求（是“全成功”还是“先到先得”或“至少一个成功”）。

示例：

```javascript
Promise.all([p1,p2]).then(results => {});
Promise.allSettled([p1,p2]).then(results => {});
Promise.race([p1,p2]).then(first => {});
Promise.any([p1,p2]).then(firstFulfilled => {});
```

### 手写一个简化版 Promise（核心功能：then 链式）

下面实现为简化版，支持链式 `.then`，基本的 resolve/reject，Promise 值穿透与 thenable 兼容（简化版，未实现所有 edge case）：

```javascript
class SimplePromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.handlers = [];

    const resolve = (val) => {
      setTimeout(() => this._resolve(val), 0);
    };
    const reject = (err) => {
      setTimeout(() => this._reject(err), 0);
    };

    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  _resolve(val) {
    if (this.state !== 'pending') return;
    if (val && (typeof val === 'object' || typeof val === 'function')) {
      // thenable 处理
      try {
        const then = val.then;
        if (typeof then === 'function') {
          return then.call(val, v => this._resolve(v), e => this._reject(e));
        }
      } catch (e) {
        return this._reject(e);
      }
    }
    this.state = 'fulfilled';
    this.value = val;
    this.handlers.forEach(h => h.onFulfilled(val));
  }

  _reject(err) {
    if (this.state !== 'pending') return;
    this.state = 'rejected';
    this.value = err;
    this.handlers.forEach(h => h.onRejected(err));
  }

  then(onFulfilled, onRejected) {
    return new SimplePromise((resolve, reject) => {
      const handle = () => {
        try {
          if (this.state === 'fulfilled') {
            if (typeof onFulfilled === 'function') {
              resolve(onFulfilled(this.value));
            } else {
              resolve(this.value);
            }
          } else if (this.state === 'rejected') {
            if (typeof onRejected === 'function') {
              resolve(onRejected(this.value)); // 注意：如果 onRejected 返回值，视为 resolve
            } else {
              reject(this.value);
            }
          } else {
            this.handlers.push({
              onFulfilled: (v) => {
                try {
                  if (typeof onFulfilled === 'function') resolve(onFulfilled(v));
                  else resolve(v);
                } catch (e) { reject(e); }
              },
              onRejected: (e) => {
                try {
                  if (typeof onRejected === 'function') resolve(onRejected(e));
                  else reject(e);
                } catch (err) { reject(err); }
              }
            });
          }
        } catch (e) { reject(e); }
      };

      // 如果已经 settled，异步执行处理
      if (this.state !== 'pending') setTimeout(handle, 0);
      else this.handlers.push({
        onFulfilled: (v) => { try { if (typeof onFulfilled === 'function') resolve(onFulfilled(v)); else resolve(v); } catch (e) { reject(e); } },
        onRejected: (e) => { try { if (typeof onRejected === 'function') resolve(onRejected(e)); else reject(e); } catch (err) { reject(err); } }
      });
    });
  }

  catch(fn) {
    return this.then(null, fn);
  }

  static resolve(v) {
    return new SimplePromise((res) => res(v));
  }

  static reject(e) {
    return new SimplePromise((_, rej) => rej(e));
  }
}
```

- **面试点**：异步执行 `.then` 回调（规范要求微任务队列），thenable 处理，链式返回一个新的 Promise。

### 实战：用 Promise 重写 `setTimeout` 任务队列

- 需求：按顺序执行一组带延迟的任务（每个任务按指定延迟执行，前一个完成才开始下一个）。
- 实现思路：用 Promise 链或 `async/await` 顺序调度。

示例（任务队列类）：

```javascript
class TimeoutQueue {
  constructor() { this.chain = Promise.resolve(); }

  // task: () => Promise|value, delay: ms
  add(task, delay = 0) {
    this.chain = this.chain.then(() => new Promise((resolve) => {
      setTimeout(() => {
        Promise.resolve().then(task).then(resolve, resolve); // 即使 task 抛错，也让队列继续（可根据需要改为 reject）
      }, delay);
    }));
    return this.chain;
  }
}

// 用法
const q = new TimeoutQueue();
q.add(() => console.log('task1'), 1000);
q.add(() => fetch('/api'), 500);
```

- 也可以把 `setTimeout` 包装为 Promise：

```javascript
const wait = ms => new Promise(res => setTimeout(res, ms));
await wait(1000);
```

### 实战：`Async / await` 错误处理的 3 种方式

1. **try / catch**（最直观）：

```javascript
try {
  const res = await foo();
} catch (err) {
  // 处理
}
```

1. **对 Promise 使用** `**.catch**`（适用于并行）：

```javascript
const p = foo();
p.catch(err => { /* 处理 */ });
```

1. **返回 [err, result] 的封装（避免 try/catch）**：

```javascript
const to = (p) => p.then(data => [null, data]).catch(err => [err]);
const [err, data] = await to(foo());
if (err) { /* 处理 */ }
```

### “异步题大汇总”：输出顺序、陷阱解析（几个常见面试题）

- 经典例子（解释输出）：

```javascript
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// 输出： start, end, promise, timeout
```

- 更复杂（微任务链）：

```javascript
Promise.resolve().then(() => {
  console.log('p1');
  Promise.resolve().then(() => console.log('p2'));
});
console.log('sync');
// 输出： sync, p1, p2
```

- `setTimeout` 与 `Promise.then` 的优先级（Promise microtask 优先于宏任务）。
- Node 特例：`process.nextTick` 比 Promise 微任务更早执行（面试可能考）。

**面试点**：能够解释任意给定代码片段的输出顺序、理解微任务与宏任务优先级、Promise 回调的异步性（即使 Promise 已经 resolved）等。

### 什么是原型？什么是原型链？

- **原型（prototype）**：每个函数（构造函数）都有 `prototype` 对象，实例对象通过 `__proto__`（或 `Object.getPrototypeOf`）指向构造函数的 `prototype`。属性查找会沿着这条链向上查找，这就是**原型链**。
- **面试点**：区分 `prototype`（函数的属性，用于实例的原型）与 `__proto__`（实例的指针）；`Object.prototype` 的 `__proto__` 是 `null`（链的终点）。

示例：

```javascript
function A() {}
A.prototype.say = function(){ console.log('hi'); };
const a = new A();
a.say(); // 查找：a -> A.prototype -> Object.prototype
```

### 构造函数与 `new` 的机制

`new` 的执行过程（面试常考）简述：

1. 创建一个新对象：`obj = {}`。
2. 将新对象的 `__proto__` 指向构造函数的 `prototype`（`obj.__proto__ = Fn.prototype`）。
3. 将构造函数内部 `this` 绑定到新对象并执行构造函数：`Fn.call(obj, ...args)`。
4. 若构造函数返回一个对象（非 null 且类型为 object 或 function），则返回该对象；否则返回 `obj`。

- **面试点**：要写出 `new` 的手写实现（见第 31 条）。

### 手写 `new` 的实现逻辑

```javascript
function myNew(fn, ...args) {
  if (typeof fn !== 'function') throw new TypeError('myNew must be called with constructor');
  const obj = Object.create(fn.prototype);
  const res = fn.apply(obj, args);
  return (res !== null && (typeof res === 'object' || typeof res === 'function')) ? res : obj;
}

// 用法
function Person(name) { this.name = name; }
const p = myNew(Person, 'Tom');
```

### `instanceof` 背后的原理

- 原理：判断构造函数的 `prototype` 是否出现在对象的原型链上。
- 手写实现：

```javascript
function myInstanceOf(obj, constructor) {
  let proto = Object.getPrototypeOf(obj);
  const prototype = constructor.prototype;
  while (proto !== null) {
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}  
```

- **面试点**：`instanceof` 与跨 global 环境（iframe）对象的问题；`Object.prototype.toString` 在类型检测上的作用。

### `Object.create` 是怎么实现继承的？

- `Object.create(proto)` 本质上创建了一个新的对象并将它的内部 `[[Prototype]]` 指向 `proto`。
- 简单 polyfill：

```javascript
function objectCreate(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}
```

- ES6 中可直接用 `Object.create(proto)`。`Object.create(null)` 创建无原型的纯字典对象。

### `class` 是语法糖吗？背后发生了什么？

- **是的**：ES6 `class` 只是构造函数 + 原型链的语法糖，提供更清晰的语法（`constructor`、`extends`、静态方法、`super` 等）。
- **背后**：`class` 声明会创建一个构造函数（不可被提升），并把实例方法放在 `.prototype` 上，把静态方法放在构造函数上。`extends` 会设置子类的 `prototype` 指向父类实例的 `prototype`（通过 `Object.create`），同时设置 `super` 调用链。
- **面试点**：`class` 的方法是不可枚举的；`class` 的 constructor 不可在非构造情境直接调用（`class` 的 `toString`、`name` 等），以及 `extends null` 的特殊情况。

示例（等价思想）：

```javascript
class A {
  constructor(x) { this.x = x; }
  foo() { return this.x; }
}
// 转换为
function A(x) { this.x = x; }
A.prototype.foo = function() { return this.x; };
```

### JS 中常见继承方式对比总结

- **原型链继承**（`Child.prototype = new Parent()`）：共享引用属性问题。
- **构造函数继承（借用构造）**（`Parent.call(this)`）：可以继承实例属性，无法继承方法（需用 prototype）。
- **组合继承**（借用构造 + 原型链）：常见且实用（constructor 调用 + prototype 链接）。
- **寄生组合继承**（优化组合继承，避免调用两次父构造）：使用 `Object.create(Parent.prototype)` 并修复 constructor。
- **ES6** `**class extends**`：语法糖，背后处理原型链与 `super`。
- **面试点**：每种方式的优缺点（比如共享引用带来的 bug、性能或重复调用父构造函数的问题），以及如何修复（使用 `Object.create`、修复 constructor 等）。

### 说说JS如何判断数据类型

> 6种判断方式

1. ===
   	undefined
     null
2. typeof
   	string
   	number
   	boolean
   	undefined
   	symbol
   	bigint
   	function
   	object
   		null
   		所有非函数对象
3. instanceof
   	A instanceof B: 判断A或者A原型链上的对象是否是B类型的实例
   	可以用于判断某个对象是否某个特定类型的
   		区别Object对象与数组对象
4. obj.constructor
   	得到对象的构造函数
   	得到number/string/boolean值的包装类型
   	null/undefined没有constructor
5. String.prototype.toString.call(obj)
   	得到构造函数的名称
6. Array.isArray()
   	专门判断数组

### 说说常见的数组方法

1. 更新数组的7个
   	push / pop / unshift / shift
   	splice
   	sort / reverse

2. 遍历元素
   	forEach
   	map / filter / reduce
   	find / findIndex
   	every / some

3. 其它
   	slice
   	concat
   	join
   	includes
   	indexOf


  - 函数调用时, 是将实参变量的数据拷贝一份赋值给形参变量

  - 只是实参变量数据可能是基本类型值 ==> 值传递

  - 也可能是引用类型的值(也就是地址值) ==> 引用传递/值传递

  - 注意下面的代码, 准确的说不是将a内存的地址赋值给b,  而是将a中保存的地址值赋值给b

    ```js
    var a = {}
    var b = a
    
    var a1 = 2
    var a2 = {}
    function fn (x) {
      
    }
    fn(a1) // 值传递
    fn(a2) // x = a2  拷贝a2内存中保存的值(地址值)传递给x
    ```

### 说说你对作用域与作用域链的理解

- 作用域
  - 一个变量可以合法使用的范围/区域
  - 作用域起到了隔离变量, 避免了变量重名冲突的问题(也就是允许了不同作用域中可以有同名的变量)
  - 分类:
    - 全局作用域
    - 函数作用域
    - 块作用域 => ES6的let或const变量

- 作用域链
  - 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
  - 本质: 包含由内向外的多个变量对象的数组  ==> 这个可以不用说
  - 当查找一个变量, 在整个作用域链中都找不到时: 会报引用错误(RefrenceError), 错误信息(message)为这个变量没有定义

### 说说变量提升与函数提升

- 变量提升(`变量声明提升`)
  - 变量声明语句会提升到当前作用域的最前面执行
  - 在变量声明语句之前, 就可以访问到这个变量(undefined)
- 函数提升(`函数声明提升`)
  - 函数声明语句提升到当前作用域的最前面执行
  - 在函数声明语句之前, 就可以执行该函数
- 原因: 简单来说就是在执行全局代码和函数前会进行`预解析/处理`
  - 将var变量声明放在最前面执行
  - 将function函数声明放在最前面执行
- 注意
  - const / let / class 没有提升
  - var 变量 / function 函数声明 才有提升

### 区分执行函数定义与执行函数

- 执行函数定义:  创建函数对象, 如果指定了函数名, 同时会定义变量并指向这个函数对象
- 执行函数: 执行函数内部的语句
- 必须先执行函数定义, 再执行函数  ===> 注意: 函数定义有可能会提升到最上面执行

## 各大互联网大厂`Vue`框架面试真题&答案

### 什么是 Vue 中的 slot？它有什么作用？

**要点**：`slot` 是组件内容分发机制（内容投影/插槽），用于父组件向子组件传入任意结构的 DOM/模板片段，使组件更具复用性和可配置性。

**常见类型**：

- 默认插槽（unnamed slot）
- 具名插槽（named slot）
- 作用域插槽 / 作用域插槽（scoped slot）——子组件向插槽提供数据，父组件接收并渲染

**示例（Vue 3 / Vue 2 语法一致）**：

子组件 `BaseCard.vue`：

```vue
<template>
  <div class="card">
    <header class="card-header">
      <slot name="header">默认头部</slot>
    </header>
    <main class="card-body">
      <slot>默认内容</slot>
    </main>
    <footer class="card-footer">
      <slot name="footer">默认底部</slot>
    </footer>
  </div>
</template>
<script>
  export default {
    name: 'BaseCard'
  }
</script>
<style scoped>
  .card{border:1px solid #eee;padding:12px;border-radius:6px}
</style>
```

父组件使用：

```vue
<template>
  <BaseCard>
    <template #header>
      <h3>我是头部</h3>
    </template>

    主体内容在这里

    <template #footer>
      <button>确定</button>
    </template>
  </BaseCard>
</template>
```

**作用域插槽示例（子提供数据，父使用）**：
子组件 `ListProvider.vue`：

```vue
<template>
  <ul>
    <slot v-for="item in items" :item="item" :index="indexOf(item)"></slot>
  </ul>
</template>
<script>
  export default {
    props: { items: Array },
    methods: { indexOf(i){ return this.items.indexOf(i) } }
  }
</script>
```

父组件：

```vue
<ListProvider :items="users">
  <template #default="{ item, index }">
    <li>{{ index }} - {{ item.name }}</li>
  </template>
</ListProvider>
```

### 在 Vue 渲染模板时，如何保留模板中的 HTML 注释？

**要点**：Vue 默认会移除模板中的 HTML 注释（`<!-- -->`）。如果需要保留注释，一般是调试或特定场景。

- 在 Vue 2 中可以使用 `v-pre` 来跳过编译（会保留注释？需谨慎 — v-pre 主要是跳过模板编译并保留原样内容）。
- 在编译阶段保留注释通常不推荐，Vue 的虚拟 DOM 不把注释视为常规节点。

**实操建议**：如果只是用于调试，直接在渲染前查看源模板或使用开发者工具。若确实需要在 DOM 中保留注释，可在 `mounted` 时手动插入注释节点。

**代码示例（手动插入注释）**：

```vue
<template>
  <div ref="container"></div>
</template>
<script>
  export default {
    mounted(){
      const comment = document.createComment('这是保留的注释');
      this.$refs.container.appendChild(comment);
    }
  }
</script>
```

### Vue 计算属性的函数名和 data 中的属性可以同名吗？为什么？

**要点**：**不可以**（或**不应**）同名。若同名，会产生冲突，后定义的会覆盖先定义的（Options API 下 Vue 会把 data、props、computed 等合并到实例上，命名冲突会警告或覆盖）。

**原理**：Vue 将 `data`、`methods`、`computed` 等代理到组件实例（`this`）上，命名冲突会导致不确定行为。`computed` 返回的是 getter (和可选 setter)，而 `data` 是响应式数据，混淆会导致难以调试。

**示例（错误写法）**：

```javascript
data() { return { count: 1 } },
computed: {
  count() { return this.count + 1 } // 会造成递归/覆盖问题
}
```

**正确做法**：使用不同命名或改为 `computedCount`。

### Vue 的 `v-show` 和 `v-if` 有什么区别？使用场景分别是什么？

**要点**：

- `v-if`：真正的条件渲染，会在条件为 `false` 时移除/销毁 DOM 节点与组件实例；开销较大（创建/销毁）但在条件很少变更时更合适。
- `v-show`：通过 CSS `display` 控制显示/隐藏，始终保留 DOM 节点与组件实例；切换开销小但初始渲染始终会创建元素。适合频繁切换的场景。

**选用建议**：

- 条件很少改变（如登录后显示某个模块）使用 `v-if`。
- 频繁切换显示/隐藏（如标签页切换、弹窗显示）使用 `v-show`。

**示例**：

```vue
<!-- v-if -->
<div v-if="showModal">
  <ModalComponent />
</div>

<!-- v-show -->
<div v-show="visible">
  <HeavyButFrequentlyToggledComponent />
</div>
```

### 如何在 Vue 中使用 `defineAsyncComponent` 实现异步组件加载？

**要点**：`defineAsyncComponent` 是 Vue 3 提供的 API（Vue 2 可使用异步组件工厂函数）。支持加载占位组件、超时、错误处理、延迟展示等。

**Vue 3 示例**：

```js
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./MyHeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,       // 延迟显示 loading（ms）
  timeout: 10000,   // 超时（ms）
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      retry()
    } else {
      fail()
    }
  }
})
```

模板中：

```vue
<template>
  <AsyncComp />
</template>
```

**Vue 2 示例（异步工厂）**：

```js
const AsyncComp = () => ({
  component: import('./MyHeavyComponent.vue'),
  loading: LoadingSpinner,
  error: ErrorDisplay,
  delay: 200,
  timeout: 10000
})
```

### 请简单介绍一下你对 Vue 的理解，包括它的特点和优势

**要点（面试回答要点）**：

- 响应式数据驱动视图（Declarative Rendering）
- 组件化（Component-based），易组合与复用
- 渐进式框架：可以逐步引入（只用于视图层或全栈）
- 生态完善：Vue Router、Vuex/Pinia、CLI、Vite 支持
- 性能：虚拟 DOM + 优化策略（Vue 3 使用 Proxy，性能更好）
- 开发体验：单文件组件（SFC）、良好文档、社区活跃

**面试包装句**：
“Vue 是一个以数据驱动视图的渐进式前端框架，强调组件化与简洁的 API，适合快速构建 SPA，同时在大型应用中也能通过组合式架构与成熟的生态（路由、状态管理、工具链）满足复杂需求。”

------

### 在 Vue 项目中，你通常如何组织和管理组件？请描述一下你的组件化开发思路

**原则**：

- 按功能/域（feature）划分目录（Domain-driven folder），而不是按类型（components/services/styles）
- 公共组件库（`/components/ui`）与业务组件（`/views` 或 `/modules/**`）分离
- 组件粒度：先抽象小且重复的 UI 组件（Button、Input、Modal），业务组件保持较高层次
- 组件命名：`PascalCase` 或 `kebab-case` 一致性
- 侧重可测试性与文档（Storybook）
- 使用 `props` + `emits` 明确数据流，尽量避免父子深层传递（使用 provide/inject 或状态管理）

**示例目录结构**：

```json
src/
  components/      # 通用 UI 组件
  modules/         # 按业务模块划分
    user/
      components/
      views/
      store/
  layouts/
  router/
  store/
```

**组件开发流程（面试回答的流程化思路）**：

1. 识别复用点 -> 抽取组件边界
2. 定义 `props/slots/emits` 合约
3. 写文档和示例（Story）
4. 编写单元测试
5. 优化样式与交互（无副作用）

### Vue 的生命周期钩子有哪些？它们在什么阶段被调用？

**Vue 2（Options API）主要钩子**：

- `beforeCreate`：实例初始化（响应式未建立）
- `created`：实例已创建（可以访问 data、methods、computed，但 DOM 未挂载）
- `beforeMount`：模板编译并挂载之前
- `mounted`：DOM 已挂载（可以访问 `$el`）
- `beforeUpdate`：响应式数据改变但 DOM 还未更新
- `updated`：DOM 已更新
- `beforeDestroy`：实例销毁前（清理定时器、事件监听）
- `destroyed`：实例已销毁

**Vue 3（名字略有变化，destroy -> unmount）**：

- `beforeUnmount` / `unmounted`（替代 beforeDestroy/destroyed）
- 对 Composition API 使用 `onMounted`, `onUnmounted` 等等

**示例（Options API）**：

```js
export default {
  created() { console.log('created') },
  mounted() { console.log('mounted') },
  beforeUnmount() { console.log('beforeUnmount') }
}
```

**示例（Composition API）**：

```js
import { onMounted, onUpdated, onUnmounted } from 'vue'
setup(){
  onMounted(()=>console.log('mounted'))
  onUpdated(()=>console.log('updated'))
  onUnmounted(()=>console.log('unmounted'))
}
```

### 谈谈你对 Vue 的响应式系统的理解，以及它是如何实现数据的双向绑定的

**要点**：

- Vue 2：使用 `Object.defineProperty` 的 getter/setter 检测属性访问与修改，不能直接检测新增/删除；通过数组方法重写（push/pop/splice）来拦截变动。
- Vue 3：采用 `Proxy`，对对象、数组等更全面地拦截（包括新增属性、删除等），性能与实现更简洁。
- 双向绑定（`v-model`）：在底层是 prop + 事件（`modelValue` + `update:modelValue` 在 Vue 3），组件监听事件并更新 prop 对应的外部数据，从而形成“类似双向”的交互。

**工作原理简述**：

1. 将数据变为响应式（Vue 2：`defineReactive` -> `Object.defineProperty`；Vue 3：`reactive` 使用 Proxy）
2. 渲染函数/模板收集依赖（在 getter 时收集当前副作用 watcher）
3. 当 setter 被触发时，通知相关 watcher 重新渲染或调用 computed/watch

**示例（Vue 3 reactive）**：

```js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
```

`v-model` 在父子组件传值上的示例（Vue 3）：
父组件：

```vue
<Child v-model="name" />
```

子组件：

````vue
<script>
  export default {
    props: ['modelValue'],
    emits: ['update:modelValue'],
    methods: {
      onInput(e){ this.$emit('update:modelValue', e.target.value) }
    }
  }
</script>
````

### 在 Vue 中，如何进行路由管理？你是否使用过 Vue Router？

**要点**：

- Vue Router 是官方路由解决方案，支持 SPA 的路由、嵌套路由、路由守卫、动态路由、懒加载等。
- 常见用法：在 `router/index.js` 定义路由表，通过 `createRouter`（Vue 3）或 `new VueRouter`（Vue 2）创建实例，挂载到 App。

**Vue 3 + Vue Router 4 示例**：

```js
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
const routes = [
  { path: '/', name:'Home', component: Home },
  { path: '/users/:id', name:'User', component: () => import('@/views/User.vue'), props: true },
]
const router = createRouter({
  history: createWebHistory(),
  routes
})
export default router
```

挂载：

```js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
```

**路由守卫示例（全局）**：

```js
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isLoggedIn()) next('/login')
  else next()
})
```

### 对于 Vue 的状态管理，你有什么经验？是否使用过 Vuex 或其他类似的状态管理库

**要点（面试回答）**：

- 大型应用推荐集中式状态管理（Vuex 或 Pinia），用于跨组件共享、时间旅行、插件生态（持久化、日志、调试）。
- 小型或局部状态可以使用组件组合、`provide/inject`、或通过组合式 API 的 `reactive` 在模块间共享。
- 我在项目中既使用过 Vuex（V2/V3 时代），也在 Vue 3 项目中使用过 Pinia（更简洁、更 TS 友好）。

**经验/实践**：

- 使用模块化（namespaced modules）划分业务状态
- 把副作用（API 请求）放在 actions（Vuex）或 stores（Pinia）的 actions 中
- 使用持久化（localStorage）只保存必要状态（token、user），避免过度持久化
- 在 SSR 场景注意状态注水/反序列化

------

### 请你说说 Pinia 和 Vuex 的不同

**要点**：

- API 风格：Pinia 更轻量、基于函数（类似组合式 API），Vuex 更偏配置式（mutations / actions / getters）。
- 写法：Pinia 使用 `defineStore`，没有 mutations 的强制分层（可直接修改 state 或通过 action 修改）；Vuex 需要通过 mutation 修改 state（更明确但冗长）。
- TypeScript 支持：Pinia 原生更友好，类型推断更自然。
- 调试：两者都支持 devtools，但 Pinia 的插件系统更简单。
- 体量与学习成本：Pinia 更小、更易上手，Vuex 适合已有大量 Vuex 代码库的中大型项目迁移成本较高。

**Pinia 示例**：

```js
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({ name: '', token: '' }),
  actions: {
    setName(n){ this.name = n },
    async login(payload){ this.token = await api.login(payload) }
  }
})
```

### 对于 Vue 的单文件组件（.vue），你对它的结构和用法有什么了解？

**要点**：SFC（Single File Component）包含三个主要块：

- `<template>`：模板，可写 HTML + 指令
- `<script>`：逻辑（Options API / Composition API / setup）
- `<style>`：样式（支持 scoped、module、lang=less/scss）

**增强功能**：

- `<script setup>`（Vue 3）更简洁、自动类型推断
- `<style scoped>`：作用域样式
- SFC 支持 `<script setup lang="ts">`、多个 `<style>` 等

**示例（Vue 3 + script setup）**：

```vue
<template>
  <button @click="inc">{{ count }}</button>
</template>
<script setup>
  import { ref } from 'vue'
  const count = ref(0)
  function inc(){ count.value++ }
</script>
<style scoped>
  button { padding:8px 12px }
</style>
```

### 请分享一些你在 Vue 项目中进行性能优化的经验和技巧

**要点与技巧**：

- 路由懒加载（按需加载页面组件）
- 组件懒渲染 / keep-alive 控制（避免不必要重复渲染）
- 使用 `v-once` 渲染静态节点
- 减少响应式对象范围：只对必要数据做 reactive/ref
- 避免在模板中做复杂计算；把逻辑放到 computed
- 使用虚拟列表（virtual-scroller）处理长列表
- 使用 `watch` 代替 `deep` 大量遍历
- 服务端压缩/HTTP2、CDN、资源预加载、Cache-Control
- 图片懒加载、SVG 优化、合并小资源、代码分割
- 对第三方库进行按需引入（lodash 按需或只引入特定函数）
- 对频繁更新的 DOM 使用 `v-show` 而非 `v-if`（见第 4 点）
- 使用性能分析工具（Chrome DevTools、Lighthouse、Vue Devtools）

**示例：路由懒加载**：

```js
const routes = [
  { path: '/home', component: () => import('@/views/Home.vue') }
]
```

### 如果要在 Vue 项目中集成第三方库或插件，你通常会采取哪些步骤？

**流程**：

1. 确认兼容性（Vue 版本、SSR/CSR）
2. 查看是否有官方或社区插件（优先）
3. 按需引入并做 Tree-shaking（避免整个包引入）
4. 在主入口 `main.js` 使用 `.use()` 或通过 provide/inject 注入
5. 封装适配层（在项目中标准化使用接口）
6. 在组件中注入/导入，写测试
7. 做性能评估与样式隔离（若会污染全局 CSS）
8. 编写使用文档与示例

**示例（注册插件）**：

```js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyLib from 'my-lib'
createApp(App).use(MyLib, { option: true }).mount('#app')
```

### 请描述一次你在 Vue 项目中遇到的挑战，并说明你是如何解决它的

**示例回答（面试型）**：

*挑战*：某项目首屏加载慢（首屏时间 > 3s），业务页面包含多个第三方图表、地图 SDK 与大量初始数据。
*分析*：通过 Lighthouse 与 Chrome Profile 定位到 JS 大包和阻塞渲染（同域 synchronous 请求、CSSOM 阻塞），以及首次渲染加载大量静态数据。
*解决措施*：

1. 路由层面做懒加载，拆分 chunk。
2. 将大型非必要 SDK（地图、图表）按需加载或延迟加载（IntersectionObserver + import()）。
3. 对接口做分页/延迟加载，首屏仅请求必要数据，次屏请求异步加载。
4. 打包优化：使用 Vite + rollup 动态拆包，并开启 gzip/ brotli；移除未用到的 polyfills。
5. 使用 SSR/预渲染（如果适合）或 HTML 预渲染部分静态内容。
   *结果*：首屏时间从 3s 降到 1.2s，用户感知显著提升。

------

### 请说说 Vue 项目中一般把数据请求这个操作放在那个位置

**要点**：

- 页面级（视图组件 `mounted` / `setup`）通常负责请求页面所需数据。
- 将通用或共享数据放到统一 store（Vuex/Pinia）的 action 中；组件触发 action 获取数据。
- 可把 API 请求封装到 `services/api.js` 层，便于复用与测试。
- SSR 场景会在服务端做数据预取（如 `asyncData` 风格）。

**示例（Composition API）**：

```js
// services/api.js
export async function fetchUser(id){ return await fetch(`/api/user/${id}`).then(r=>r.json()) }

// 页面组件
import { onMounted, ref } from 'vue'
import { fetchUser } from '@/services/api'
setup(){
  const user = ref(null)
  onMounted(async ()=> { user.value = await fetchUser(1) })
  return { user }
}
```

### Vue 组件之间的通信方式有哪些？

**列表**：

- 父子组件：`props` / `emit`
- 兄弟组件：通过父组件中转 props 或使用事件总线（不推荐），或使用状态管理（Pinia/Vuex）
- 跨层级：`provide` / `inject`
- 全局状态管理：Vuex / Pinia
- 自定义事件总线（EventEmitter）——小型场景可用，但易混乱
- $attrs / $listeners（透传属性/事件）
- 插槽（slot）与作用域插槽（child->parent 数据流）
- DOM 自定义事件（极少使用）

**示例（父子）**：

```js
<!-- Parent -->
<Child :value="count" @update="val => count = val" />
```

### 为什么 `data` 属性是一个函数而不是一个对象？

**要点**：组件是可复用的构造函数。若 `data` 是对象，则多个组件实例会共享同一对象，互相污染状态。将 `data` 写为函数可以为每个组件实例返回独立对象，确保实例隔离。

**示例**：

```js
data(){ return { count: 0 } } // 每个实例都有自己的 count
```

### 动态给 Vue 的 data 添加一个新的属性时会发生什么？怎样解决？

**问题**：在 Vue 2 中，直接 `this.someNewProp = value` 不会使新属性成为响应式（因为 `Object.defineProperty` 在初始化时未拦截新增属性）。
**解决办法**：

- Vue 2：使用 `Vue.set(this.obj, 'newKey', value)` 或 `this.$set(this.obj, 'newKey', value)`。
- Vue 3（Proxy）：新增属性是响应式的，直接赋值生效。
- 另外的设计建议：尽量在 `data` 中先声明所有预期字段，或使用 `reactive` 包装对象并初始化默认值。

**示例（Vue 2）**：

```js
this.$set(this.user, 'age', 25) // 响应式
```

### 说说你对 Vue 的 mixin 的理解，有什么应用场景？

**要点**：Mixin 是一种复用组件逻辑的方式，将一组选项（data、methods、created 等）混入目标组件。但 mixin 会引入命名冲突和隐式依赖，难以追踪数据来源。

**应用场景**：

- 旧项目中快速复用通用逻辑（如日志、通用方法）
- 当逻辑很小且重复，且不适合状态管理时可短期使用

**替代方案（更推荐）**：

- Composition API（`useXxx` hooks）更明确、可组合、类型安全

**示例 mixin**：

```js
// mixins/logger.js
export default {
  created(){ console.log('组件创建', this.$options.name) },
  methods: {
    $log(msg){ console.log(msg) }
  }
}
```

使用：

```js
import logger from '@/mixins/logger'
export default { mixins: [logger] }
```

### Vue 常用的修饰符有哪些，有什么应用场景？

**事件修饰符（常用）**：

- `.stop`：`@click.stop` 阻止事件冒泡
- `.prevent`：`@submit.prevent` 阻止默认事件
- `.capture`：使用事件捕获
- `.self`：只有事件目标是元素本身才触发（常用于遮罩层）
- `.once`：只触发一次

**按键修饰符**：

- `.enter`、`.esc`、`.space` 等或自定义键码 `.keyCode`

**表单修饰符**：

- `.lazy`：失去焦点或 change 时更新（默认 input 实时）
- `.number`：把输入转为 Number
- `.trim`：去掉首尾空格

**指令修饰符**：

- `v-bind` 的 `.prop`, `.camel`（用于属性名转换）

**示例**：

```js
<input v-model.lazy="name" @keyup.enter="onEnter" />
<button @click.stop="onClick">点我</button>
```

### Vue 的 `computed` 和 `watch` 有什么区别？

**要点**：

- `computed`：基于依赖缓存的计算属性，适合用于模板绑定或衍生数据；只有相关依赖变化时才重新计算。
- `watch`：观察数据变化并执行副作用（异步或开销较大操作），适合数据变化触发异步请求或手动执行逻辑。

**使用建议**：

- 需要返回值并用于模板/其他计算用 `computed`
- 需要执行副作用（API 调用、手动 DOM 操作）用 `watch`

**示例**：

```js
computed: {
  fullName(){ return `${this.first} ${this.last}` }
},
watch: {
  query: {
    handler: _.debounce(function(val){ this.fetch(val) }, 300),
      immediate: true
  }
}
```

### 有写过自定义指令吗？自定义指令的应用场景有哪些？

**常见场景**：

- 直接操作 DOM：如自动聚焦 `v-focus`、节流滚动监听、拖拽、权限指令控制元素显示
- 第三方库 DOM 集成（如直接在元素上挂载某 SDK）

**Vue 3 自定义指令示例（**`**v-focus**`**）**：

```js
// directives/focus.js
export default {
  mounted(el){ el.focus() },
  updated(el){ /* 可选逻辑 */ }
}

// main.js
app.directive('focus', FocusDirective)
```

模板使用：

```vue
<input v-focus />
```

### SPA 首屏加载速度慢的怎么解决？

**策略汇总**：

- 路由懒加载 & 代码分割（减少首包体积）
- 延迟/按需加载第三方 SDK（地图、图表）
- 减少初始请求：分页、首屏只请求必要数据
- 使用 `prefetch/preload` 或 HTTP/2、CDN、压缩（gzip/brotli）
- SSR 或预渲染以减少白屏（若适用）
- 使用 `resource hints`（`<link rel="preload">`）
- 优化图片：缩放、WebP、懒加载
- 去掉/延迟执行阻塞 JS（critical JS inlined only）
- 使用浏览器缓存、长缓存策略
- 使用性能指标追踪（Lighthouse, RUM）持续监控

### Vue 3 有了解过吗？能说说跟 Vue 2 的区别吗？

**核心差异**：

- 响应式实现：`Proxy`（Vue3） vs `Object.defineProperty`（Vue2）
- Composition API：更好的逻辑复用与 TypeScript 支持
- 性能提升：更小 runtime、更快的虚拟 DOM
- Tree-shaking 更友好，生态重写以支持按需引入
- 新的生命周期钩子命名（beforeUnmount/unmounted）
- 更好的 TypeScript 支持与 `<script setup>`

### Vue 3 为什么使用 Proxy 拦截数据？

**要点**：

- Proxy 能拦截更多操作（属性读取、写入、删除、枚举、原型操作等），解决 Vue 2 在新增/删除属性和数组索引上的限制。
- Proxy 性能更优，允许更简洁的实现，且代码更平台友好（更少 polyfill）。

### Vue 3 新增特性

**主要新增**（面试摘要）：

- Composition API（`setup`、`ref`、`reactive`、`computed`）
- `<script setup>` 语法糖
- `Teleport`、`Suspense` 组件
- 更快的虚拟 DOM、Proxy 响应式系统
- 更好的 TypeScript 支持
- 新的生命周期钩子和 API（`onMounted` 等）

### 如何看待 Composition API 和 Options API

**对比与看法**：

- Options API：结构化清晰，适合小团队或上手快的场景，代码可读性对初学者友好。
- Composition API：更适合复杂逻辑、逻辑复用（hooks 风格）、TypeScript 支持好。将相关逻辑放在一起，便于维护。
- 实践建议：在团队中可混合使用（Vue 3 支持），对新项目推荐 Composition API（长远收益），对老项目可逐步迁移。

### Vue 3.0 编译做了哪些优化？

**要点**：

- 静态提升（static hoisting）：把静态节点抽离，减少渲染开销
- 编译时静态树标记：优化 patch 流程
- 更细粒度的依赖跟踪与副作用收集
- 编译器输出更轻量（支持 tree-shaking）

### `watch` 和 `watchEffect` 的区别？

**要点**（Vue 3）：

- `watch`：显式依赖，传入要观察的响应式源（ref、reactive、getter），更适合观察特定变量并执行副作用；可以设置 `flush`（sync/pre/post）等。
- `watchEffect`：自动收集依赖（在回调中引用的响应式值），适合立即执行并响应依赖变化的场景；不是用于对比前后值（`watch` 能获得 old/new）。

**示例**：

```js
// watch
watch(()=>state.count, (newVal, oldVal) => console.log(newVal, oldVal))

// watchEffect
watchEffect(()=> { console.log(state.count) })
```

### 如何理解 `reactive`、`ref`、`toRef` 和 `toRefs`？

**要点**：

- `ref(value)`：创建一个响应式的引用对象，包含 `.value`。用于基本类型或需要单独引用的值。
- `reactive(obj)`：将对象变为响应式代理（Proxy），直接使用对象属性，不需要 `.value`。
- `toRef(obj, key)`：把 `obj[key]` 转为一个 `ref`，用于把 reactive 对象中的某个属性以 ref 的方式传递或解构而不丢失响应性。
- `toRefs(obj)`：把 reactive 对象的每个属性都转换成 ref（常用于解构 reactive 对象而不丢失响应性）。

**示例**：

```js
import { reactive, ref, toRef, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Alice' })
const countRef = toRef(state, 'count') // countRef.value == state.count

const { count, name } = toRefs(state) // 解构后仍然响应式，count.value 对应原 state.count

const n = ref(3) // 基本类型 ref
```

### Vue 的设计哲学 & 核心理念

**答案要点（面试话术）**

- 简单与渐进式：从 CDN 引入到完整框架，学习曲线平滑。
- 以声明式渲染为中心：模板 -> 数据驱动视图。
- 组件化：把 UI 拆成可复用、隔离的组件。
- 响应式系统为核心：自动追踪依赖、最小更新单元。
- 开发者体验优先：直观 API、良好错误提示、丰富生态（CLI、Router、Pinia）。

**可补充谈点**

- Vue 把可维护性、性能、开发体验做了折中（例如模板语法更直观）。
- Vue 3 的设计目标：更小的体积、更快的性能、易于树摇（tree-shakeable）。

**示例（谈话示例）**

“我会把 Vue 的核心归结为：声明式 + 组件化 + 响应式。开发上，我优先把复杂逻辑拆组件、用 Composition API 管理逻辑复用，借助 Pinia 做状态明确化。”

### Composition-API 与响应式系统整体介绍

**答案要点**

- Composition API（`setup()`）提供按功能/业务组织逻辑的能力，便于复用（hook 风格）。
- 响应式系统包含 `reactive`, `ref`, `computed`, `watch` 等，底层基于 `Proxy` 做属性拦截与依赖收集。
- `setup()` 在组件实例创建阶段执行，返回的数据会暴露给模板。

**面试话术**

“Composition API 解决了 Options API 在大型组件中逻辑散落的问题，使关注点按功能聚合，便于测试和复用。”

**Demo**

```vue
<!-- Composition API 基本示例 -->
<template> 
  <div>
    <p>count: {{ count }}</p>
    <p>double: {{ double }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
  import { ref, computed } from 'vue';
  export default {
    setup() {
      const count = ref(0);
      const double = computed(() => count.value * 2);
      function increment() { count.value++; }
      return { count, double, increment };
    }
  };
</script>
```

### Composition-API 基础语法讲解

**重点 API**

- `setup(props, ctx)`：初始化逻辑，返回对象暴露给模板。
- `ref(value)`：创建基本类型响应式引用。
- `reactive(obj)`：创建深层响应式对象。
- `computed(fn)`：计算属性，带缓存。
- `watch(source, cb, options)`：监听副作用。
- `onMounted`, `onUnmounted` 等生命周期钩子（在 setup 内使用）。

**示例（组合使用）**

```vue
<template>
  <div>
    <input v-model="form.name" placeholder="name" />
    <p>Hello, {{ greeting }}</p>
    <button @click="submit">Submit</button>
  </div>
</template>

<script>
  import { reactive, computed } from 'vue';
  export default {
    setup() {
      const form = reactive({ name: '' });
      const greeting = computed(() => form.name ? `Hi ${form.name}` : 'Please input name');
      function submit() { alert(greeting.value); }
      return { form, greeting, submit };
    }
  };
</script>
```

### Vue 的响应式陷阱

**常见陷阱（面试应答点）**

- 直接解构 `reactive` 对象会失去响应性：`const { a } = reactiveObj` 会脱离响应系统。
- `ref` 包装对象时，访问嵌套属性可能需要 `.value` 或用 `toRefs`/`reactive`。
- 在模板内直接操作 `v-for` 的索引或 key 使用不当导致重用/渲染问题。
- 对数组直接更改索引（例如 `arr[3] = x`）在 Vue 2 有问题，Vue 3 的 Proxy 已修复大部分，但需注意赋值与变更触发场景。
- 深度监听对象时容易造成性能问题。

**修复示例**

```javascript
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, nested: { val: 1 } });
// 错误做法（会失去响应）
const { nested } = state; // nested 不再是响应式的 proxy 引用
// 正确做法
const { nested: nestedRef } = toRefs(state); // nestedRef 是 ref，保持响应
```

### Vue 响应式底层原理 — Proxy（简述）

**核心要点**

- Vue3 使用 `Proxy` 为对象创建代理，拦截 `get`/`set` 等操作。
- `get` 时收集依赖（在依赖收集阶段记录当前 activeEffect -> key）。
- `set` 时触发已订阅的 effect 执行（调度更新）。
- `Proxy` 支持对数组、Map/Set、动态添加属性等更好支持（相较于 Vue2 的 `defineProperty`）。

**示例（手写极简版本，面试中可以白板/口述）**

```java
// 极简依赖收集示意（非完整实现）
const bucket = new WeakMap();
let activeEffect = null;
function effect(fn) {
    activeEffect = fn; fn(); activeEffect = null;
}
function reactive(obj) {
    return new Proxy(obj, {
                     get(target, key) {
        if (!activeEffect) return Reflect.get(target, key);
    let depsMap = bucket.get(target);
    if (!depsMap) bucket.set(target, (depsMap = new Map()));
    let deps = depsMap.get(key);
    if (!deps) depsMap.set(key, (deps = new Set()));
    deps.add(activeEffect);
    return Reflect.get(target, key);
},
set(target, key, value) {
    const res = Reflect.set(target, key, value);
    const depsMap = bucket.get(target);
    if (depsMap) {
        const deps = depsMap.get(key);
        if (deps) deps.forEach(fn => fn());
    }
    return res;
}
});
}
```

### Vue 响应式底层原理 — 依赖追踪机制

**要点**

- 依赖收集阶段：当执行 `effect`（即组件渲染或 computed/watcher）时，全局记录当前 activeEffect，在 `get` 中把 activeEffect 存到对应 key 的依赖集合。
- 触发阶段：`set` 时查找对应 key 的依赖，依次执行/调度（队列化、去重、异步刷新）。
- 调度策略：Vue 会把更新放入微任务或 nextTick 队列（批量更新，避免重复渲染）。
- computed 是懒求值：只有被读取时才计算，并在依赖变化时标记为 dirty。

**代码示例：computed 实现核心（伪代码）**

```javascript
function computed(getter) {
  let value;
  let dirty = true;
  const runner = () => {
    if (dirty) { value = getter(); dirty = false; }
    return value;
  };
  effect(() => {
    getter(); // 触发依赖收集
    dirty = true; // 当依赖变化时，effect 会把 dirty 置 true
  });
  return { get value() { return runner(); } };
}
```

### Vue 响应式更新 vs React 状态更新

**对比要点（面试回答结构化）**

- Vue：基于依赖追踪，细粒度更新（只更新受影响的组件/模板片段）；响应式对象追踪属性级别。
- React：基于不可变状态（setState / useState）触发组件重新渲染，组件内所有依赖都会重新执行（整体函数组件重新运行），需借助 memo/pureComponent 做局部优化。
- 优点/缺点：

- - Vue 的优点：更高效的局部更新、模板更直观。缺点：当大量副作用和复杂依赖时，调试依赖链可能复杂。
  - React 的优点：模型简单（全量重新渲染）、易于推理与测试。缺点：可能造成不必要的重复计算，需要额外优化。

**面试话术**

“面试可以举例：修改对象的某个属性，在 Vue 中只会触发使用该属性的 watcher 或组件片段重新渲染；在 React 中默认会导致整个组件函数运行一次，除非用 memo/selector 等做优化。”

**示例对比（伪示意）**

- Vue:

```html
<p>{{ user.name }}</p> <!-- 只有 name 被依赖，修改 age 不会重新渲染该绑定 -->
```

- React:

```javascript
function Profile({ user }) {
  return <p>{user.name}</p>; // 只要父组件 setState 导致 user reference 变化，Profile 会重新渲染
}
```

### Vue 常用的生命周期函数

**列表（setup 环境下）**

- `beforeCreate` / `created`（Options API，setup 前后）
- `setup()`（执行点）
- `onBeforeMount`、`onMounted`
- `onBeforeUpdate`、`onUpdated`
- `onBeforeUnmount`、`onUnmounted`
- `onActivated` / `onDeactivated`（keep-alive）
- `onErrorCaptured`

**示例**

```javascript
import { onMounted, onUnmounted } from 'vue';
export default {
  setup() {
    onMounted(() => console.log('mounted'));
    onUnmounted(() => console.log('unmounted'));
    return {};
  }
};
```

**面试话术**

“常见问题：mounted 里访问 DOM 合理，created/ setup 更适合做数据初始化。onBeforeUnmount 可用于清理定时器或取消订阅。”

### Vue 的副作用清理逻辑

**要点**

- `watch` 返回一个停止函数：`const stop = watch(...); stop();`
- `onUnmounted` 用于组件销毁时清理副作用（事件监听、定时器、订阅）。
- `watchEffect` 的回调接收 `onInvalidate`，用于清理上一次副作用（例如取消请求）。
- `effect`/`computed` 内也有调度与清理机制（computed 的缓存、watch 的 lazy/flush 选项）。

**示例：watchEffect 清理**

```javascript
import { watchEffect } from 'vue';
setup() {
  const stop = watchEffect((onInvalidate) => {
    const ctrl = new AbortController();
    fetch('/api/data', { signal: ctrl.signal }).then(...);
    onInvalidate(() => { ctrl.abort(); }); // 清理上一次的 fetch
  });
  return { stop };
}
```

### 模板语法 — `v-if``v-for``v-bind``v-model`

**要点与注意**

- `v-if` 与 `v-for`：**尽量不要把 v-if 放在 v-for 上的同一元素**，否则会在每次迭代执行条件判断。优先在父层做过滤。
- `v-bind` 缩写 `:`。可以绑定对象 `:class`、`:style`、`v-bind="props"`（属性展开）。
- `v-model` 在 Vue3 支持多个参数和自定义 prop/event 名（`v-model:foo`），`modelValue` 与 `update:modelValue` 约定。
- `key` 在 `v-for` 中很重要，避免复用错误 DOM。尽量使用稳定唯一的 id。

**示例**

```vue
<!-- 不推荐 -->
<li v-for="item in list" :key="item.id" v-if="item.show">{{ item.text }}</li>

<!-- 推荐 -->
<li v-for="item in filteredList" :key="item.id">{{ item.text }}</li>
```

**v-model 自定义组件**

```vue
<!-- Parent -->
<CustomInput v-model="value" />

  <!-- Child -->
  <script>
    export default {
      props: { modelValue: String },
      emits: ['update:modelValue'],
      setup(props, { emit }) {
        const onInput = e => emit('update:modelValue', e.target.value);
        return () => h('input', { value: props.modelValue, onInput });
      }
    };
  </script>
```

### Vue 的通信基础

**方式**

- 父子：props + events（emit）。
- 兄弟/跨层级：全局状态管理（Pinia）、provide/inject、event bus（不推荐）。
- 插槽：父向子传渲染内容（可配 scoped slot 实现数据回传）。
- Router params / query 用于页面级通信。

**面试话术**

“首选 props + event；复杂跨多层共享状态优先 Pinia 或 provide/inject（只作依赖注入，不做全局状态替代）。”

**代码示例（父子通信）**

```vue
<!-- Parent.vue -->
<Child :count="count" @inc="count++" />
  <!-- Child.vue -->
  <script>
    export default {
      props: ['count'],
      emits: ['inc'],
      template: `<div>{{ count }} <button @click="$emit('inc')">+1</button></div>`
    };
  </script>
```

### Provide-Inject 的用法

**要点**

- `provide` 在上层组件提供值，`inject` 在子组件任意深度获取。
- 常用于插件、主题、依赖注入（但不用于频繁变更的共享状态，因不是响应式，除非提供 `ref`/`reactive`）。
- 可以传默认值。

**示例**

```js
// Provider.vue
import { provide, ref } from 'vue';
setup() {
  const theme = ref('dark');
  provide('theme', theme); // 提供 ref 保持响应
  return {};
}

// Consumer.vue
import { inject } from 'vue';
setup() {
  const theme = inject('theme', ref('light'));
  return { theme };
}
```

### 插槽 slot 与具名插槽

**要点**

- 默认插槽（`<slot/>`）用于未命名的内容。
- 具名插槽：`<slot name="header"/>` 与父组件 `<template #header>...</template>`。
- 插槽由父提供，子决定渲染位置，适合高阶组件或容器组件布局。

**示例**

```vue
<!-- MyCard.vue -->
<template>
  <div class="card">
    <header><slot name="header"></slot></header>
    <section><slot></slot></section>
    <footer><slot name="footer"></slot></footer>
  </div>
</template>

<!-- 使用 -->
  <MyCard>
    <template #header>Title</template>
  Main content
    <template #footer>Footer</template>
    </MyCard>
```

### 插槽作用域（Scoped Slots）

**要点**

- 子组件通过 `<slot :data="x">` 向父传数据，父用 `<template #default="{ data }">` 解构接收。
- 用于渲染高可配置性内容（像表格、列表项自定义渲染）。

**示例**

```vue
<!-- List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item">{{ item.text }}</slot>
    </li>
  </ul>
</template>

<!-- Parent.vue -->
<List :items="things">
  <template #default="{ item }">
    <strong>{{ item.name }}</strong> - {{ item.desc }}
  </template>
</List>
```

### Vue-Router 的基本配置与使用

**要点**

- 创建 router：`createRouter({ history: createWebHistory(), routes })`。
- 路由文件化建议：按页/模块组织 `views/`。
- 在组件内可用 `useRouter()`（编程式导航）和 `useRoute()`（读取当前 route）。

**示例**

```javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
const routes = [
  { path: '/', component: Home, name: 'home' },
  { path: '/about', component: About, name: 'about' },
];
export const router = createRouter({ history: createWebHistory(), routes });

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { router } from './router';
createApp(App).use(router).mount('#app');
```

### Vue-Router 的路由参数与传参方式

**方式**

- 动态路由 params：`/user/:id` -> `route.params.id`
- query：`/search?q=vue` -> `route.query.q`
- props 传参（组件接收 props）：`{ path: '/user/:id', component: User, props: true }`
- 编程式导航传参：`router.push({ name: 'user', params: { id: 1 }, query: { q: 'x' } })`（注意 params 与 name 搭配更稳）

**示例**

```javascript
// route config
{ path: '/user/:id', name: 'user', component: User, props: true }

// User.vue
export default {
  props: ['id'],
  setup(props) {
    // props.id 可用，无需从 route 里取
  }
};
```

### Vue-Router 的路由守卫

**类型**

- 全局前置守卫 `router.beforeEach`
- 全局解析守卫 `router.beforeResolve`
- 全局后置守卫 `router.afterEach`
- 单路由守卫 `beforeEnter`
- 组件内守卫 `beforeRouteEnter`, `beforeRouteUpdate`, `beforeRouteLeave`

**示例：全局守卫做鉴权**

```javascript
router.beforeEach((to, from, next) => {
  const requiresAuth = to.meta.requiresAuth;
  const loggedIn = Boolean(localStorage.getItem('token'));
  if (requiresAuth && !loggedIn) next({ name: 'login', query: { redirect: to.fullPath } });
  else next();
});
```

### Vue-Router 的路由元信息

**要点**

- 在路由配置里使用 `meta` 字段（如 `meta: { requiresAuth: true, title: '首页' }`）。
- 常用于权限校验、动态标题、面包屑或布局选择。
- 可在 `beforeEach` 或 `afterEach` 使用 `to.meta`。

**示例**

```javascript
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true, layout: 'admin' } }
```

### Vue 何时需要全局状态

**判断点（面试回答）**

- 多个组件或页面需要共享同一份数据（用户信息、权限、主题、购物车）。
- 需要跨层级或跨页面的可预测状态管理（不会频繁变化的小配置信息可用 provide/inject）。
- 要实现时间旅行 / 可追踪变更或复杂缓存策略时（更倾向使用状态管理库如 Pinia）。

**面试话术**

“若仅父子传递则不用全局；若是多页面、多组件共享并且需要统一操作（登录态、购物车），就上 Pinia。”

### Pinia 基本用法 — store

**示例：简单 store**

```js
// stores/counter.js
  import { defineStore } from 'pinia';
  export const useCounterStore = defineStore('counter', {
    state: () => ({ count: 0 }),
    getters: {
      double: (state) => state.count * 2
    },
    actions: {
      increment() { this.count++; }
    }
  });

  // main.js
  import { createApp } from 'vue';
  import { createPinia } from 'pinia';
  const pinia = createPinia();
  createApp(App).use(pinia).mount('#app');

  // 组件中使用
  import { useCounterStore } from '@/stores/counter';
  export default {
    setup() {
      const counter = useCounterStore();
      return { counter };
    }
  };
```

### Pinia 三大核心概念

**三大要点**

1. State：可响应的数据源（`state()`）。
2. Getters：基于 state 的计算（类似 computed）。
3. Actions：改变 state 的方法（可以是异步），内部 `this` 指向 store 实例。

**面试话术**

“Pinia API 简洁，支持模块化、TypeScript 友好，且 store 实例可直接在组件中使用，便于聚合与测试。”

### Pinia — Store 的响应式解构（常见问题）

**问题**

- 直接解构 store（如 `const { count } = useStore()`）会脱离响应（变成普通值）。

**正确做法**

- 使用 `storeToRefs` 把 store 的 state -> refs，或直接在模板中使用 `store.count`。

```javascript
import { storeToRefs } from 'pinia';
const store = useCounterStore();
const { count } = storeToRefs(store); // count 是 ref，保持响应性
```

### Pinia 最小可变点原则

**要点**

- 把可变状态集中到最小范围（store），其余尽量使用纯计算与组合函数，减少副作用。
- 设计时思考：哪些是源状态（single source of truth），哪些是派生状态（computed）。
- 有利于调试（time-travel、快照）与可测试性。

**面试话术**

“实际项目中把真实的可变数据放 store，把 UI 局部状态留在组件，能减少冲突与不必要重渲染。”

### Vue 的自定义指令

**用途**

- 操作 DOM（聚焦、悬浮提示、长按等），需要直接 DOM 操作时用指令。

**API**

- `beforeMount`, `mounted`, `beforeUpdate`, `updated`, `beforeUnmount`, `unmounted`。

**示例（v-focus）**

```vue
// directives/focus.js
  export default {
    mounted(el) { el.focus(); }
  };

  // main.js
  app.directive('focus', import('./directives/focus.js'));

  // 使用
  <input v-focus />
```

### Vue 模板编译流程（概述）

**步骤**

1. **模板解析（parse）**：将模板字符串解析为 AST（抽象语法树）。
2. **转换（transform）**：对 AST 做静态提升、v-for/v-if 转换、slot 编译等优化。
3. **代码生成（codegen）**：生成渲染函数（`render`），包含 `createVNode` 的调用。
4. **运行时（runtime）**：执行 render，触发响应式依赖收集与 DOM patch。

**面试话术**

“重要优化点：静态提升（static hoist）减少渲染成本；编译时把模板转换为高效 JS 代码以便 runtime 复用。”

### Vue 的 diff 算法

**要点**

- Vue 的 patch 算法参考了 Snabbdom：先对比同层节点（同 key/tag），处理 DOM 复用、props 更新、children diff。
- 对 children 使用双端指针优化（头尾对比）并对中间区使用 key -> index map 进行最少移动（最长递增子序列 LIS）以最少 DOM 操作实现移动最小更新。
- Vue3 在编译阶段生成更高效的 VNode shape & 静态标记，减少 runtime diff 工作。

**面试话术**

“简述时突出：双端比较 + key map + LIS 最小移动策略，是面试中的高分点。”

### `nextTick` 的作用是什么？

**要点**

- `nextTick` 用于在 DOM 更新完成、视图刷新后执行回调（在当前事件循环的微任务/宏任务之后）。
- 场景：在修改响应式数据后需要读取更新后的 DOM（例如获取元素尺寸、聚焦、滚动）。
- Vue 内部的 DOM 更新是异步批量的，`nextTick` 保证在 DOM 更新并渲染完成后执行。

**示例**

```vue
<template>
  <div ref="box">{{ text }}</div>
  <button @click="change">Change</button>
</template>

<script>
  import { ref, nextTick } from 'vue';
  export default {
    setup() {
      const text = ref('a');
      const box = ref(null);
      async function change() {
        text.value = 'longer text';
        await nextTick(); // 等待 DOM 更新完成
        console.log(box.value.getBoundingClientRect()); // 获取最新尺寸
      }
      return { text, box, change };
    }
  };
</script>
```

### 区别MVVM与MVC

- MVVM: 前台的技术
  - M： Model模型, 也就是包含数据的js对象   ==> data对象
  - V： View视图，动态显示模型对象中的数据的页面(前台渲染)   ==> 模板页面
  - VM： ViewModel视图模型, 通过vm读取model中的数据显示到view上, 同时view输入数据改变, vm也可以将输入数据保存到model中   ==>  Vue/组件的实例
- MVC: 后台的技术
  - M: Model（模型）包含从数据库中查询得到的数据的对象
  - V: View（视图）动态显示模型对象中的数据的页面(后台渲染)
  - C: Controller（控制器）接收用户提交的请求参数, 操作数据库生成动态数据并产生模型对象

### Vue.use()做了什么

- 对象插件: 调用插件对象install方法(传入Vue)来安装插件(执行定义新语法的代码)
- 函数插件: 直接将其作为install来调用(传入Vue)来安装插件(执行定义新语法的代码)

## 各大互联网大厂React框架面试真题&答案

### 如何理解 `UI = f(state)` 这个公式？

**解释（核心思想）**：UI 是应用状态（state）在某个时刻的纯函数映射。给定相同的 state 输入，渲染结果应该一致；渲染逻辑不要产生副作用。把 UI 理解为“状态的视图”有助于构建可预测、可测试的界面——当 state 改变时，框架负责把 UI 从旧状态更新到新状态（差分/重渲染）。

**面试要点**：强调纯函数、可重现性、单向数据流、不可变性带来的简单比较（引用 shallow compare 优势）。

**代码 demo**：

```jsx
function Counter({ count }) {
  // Counter 是 UI = f(state) 的直接例子
  return <div>Count: {count}</div>;
}

// 使用
function App() {
  const [count, setCount] = React.useState(0);
  return (
    <>
      <Counter count={count} />
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </>
  );
}
```

### 虚拟 DOM（Virtual DOM）是什么？它解决了哪些实际问题？

**解释**：虚拟 DOM 是 JS 对实际 DOM 的轻量内存表示（通常是树形对象）。当 state 改变时，框架先基于新的状态生成一棵新的虚拟 DOM，然后对新旧虚拟 DOM 做差分（reconciliation），只把必要的最小修改（patch）应用到真实 DOM。它解决了：频繁操作真实 DOM 的性能开销、手工高复杂度 DOM 更新逻辑以及跨平台渲染抽象（如服务端渲染、原生渲染）。

**面试要点**：区别“直接操作 DOM”与“构建树->差分->最小更新”的优势；并说明虚拟 DOM 并非万能（正确的性能策略还需避免过度重渲染、使用键 key 等）。

**代码 demo（手写简化版本的虚拟 DOM diff）**：

```jsx
// 极简：虚拟节点表示
function h(type, props, ...children) {
  return { type, props: props || {}, children };
}

// 简单 render -> DOM（递归）
function render(vnode, container) {
  if (typeof vnode === 'string' || typeof vnode === 'number') {
    container.appendChild(document.createTextNode(String(vnode)));
    return;
  }
  const el = document.createElement(vnode.type);
  for (const [k, v] of Object.entries(vnode.props || {})) {
    el.setAttribute(k, v);
  }
  vnode.children.forEach(child => render(child, el));
  container.appendChild(el);
}

// 用法：
const vnode = h('div', {id: 'app'}, 'hello', h('span', null, 'world'));
render(vnode, document.getElementById('root'));
```

### 说说你对 JSX 的理解

**解释**：JSX 是 JavaScript 的语法扩展，编写起来像 HTML，但本质是 `React.createElement(type, props, ...children)` 的语法糖。JSX 提高可读性、把组件结构表达为声明式树，并且允许在模板中直接使用 JS 表达式。编译工具（Babel/TypeScript）把 JSX 转成普通 JS 对象（React 元素）。

**面试要点**：解释 JSX 并不是模板引擎，强调表达式能力（花括号）、条件渲染、列表渲染与 key、以及与类型系统（TSX）的协作。

**代码 demo**：

```jsx
// JSX 写法
function Greeting({ name }) {
  return <h1>Hello, {name ?? 'stranger'}!</h1>;
}

// 编译后大致等价于：
function GreetingCompiled(props) {
  return React.createElement('h1', null, 'Hello, ', props.name ?? 'stranger', '!');
}
```

### 函数组件和类组件的本质区别是什么？

**解释**：本质上两者都是将 props 映射到 UI 的函数式抽象，但区别在于实现与生命周期模型：

- 类组件：通过 `class Component extends React.Component`，生命周期方法为实例方法（constructor、componentDidMount 等），内部使用 `this.state`，`this` 语义。
- 函数组件：本质上是函数（无实例），通过 Hooks（如 `useState`、`useEffect`）管理状态与副作用。Hooks 提供更灵活的组合能力、避免 `this` 相关陷阱，并鼓励小粒度复用。

React 团队的方向（从 React 16.8 后）是以函数组件 + Hooks 为主流，类组件仍被支持但不推荐新写。

**代码 demo（同功能对比）**：

```jsx
// 类组件
class TimerClass extends React.Component {
  state = { t: 0 };
  componentDidMount() { this.id = setInterval(() => this.setState(s => ({t: s.t + 1})), 1000); }
  componentWillUnmount() { clearInterval(this.id); }
  render() { return <div>{this.state.t}</div>; }
}

// 函数组件 + Hooks
function TimerFunc() {
  const [t, setT] = React.useState(0);
  React.useEffect(() => {
    const id = setInterval(() => setT(v => v + 1), 1000);
    return () => clearInterval(id);
  }, []);
  return <div>{t}</div>;
}
```

### 为什么 React 中 props 不可变（immutable）？

**解释**：props 是父组件下发给子组件的只读输入参数。不可变性保证了组件之间的边界清晰——子组件不会悄然改变父组件的数据源。此外，不可变数据便于进行浅比较（reference compare）从而优化渲染（`shouldComponentUpdate` / `PureComponent` / `memo`），减少不必要的重渲染。若需要改变父数据，应通过回调把意图通知父组件由父修改状态。

**代码示例**（错误示范 + 正确做法）：

```jsx
// 错误（不要直接修改 props 对象内部）
function Child({ obj }) {
  // BAD: mutating props
  obj.count = obj.count + 1; // 切勿这样做
  return <div>{obj.count}</div>;
}

// 正确（通过回调向上通知）
function Parent() {
  const [obj, setObj] = React.useState({count: 0});
  return <Child obj={obj} onInc={() => setObj(o => ({...o, count: o.count + 1}))} />;
}
function Child({ obj, onInc }) {
  return <button onClick={onInc}>{obj.count}</button>;
}
```

参见：不可变数据便于引用比较与性能优化（可理解参考 React 文档）。[bennadel.com+1](https://www.bennadel.com/blog/2903-why-should-i-care-about-immutable-data-in-reactjs.htm?utm_source=chatgpt.com)

### React 的 Fiber 架构主要是为了解决什么问题？

**解释**：Fiber 是对 React 内部 reconciler 的重写，目标是支持**增量渲染**与**可中断/优先级调度**。在早期版本 React 在执行大量同步渲染时会阻塞主线程，影响动画与交互体验。Fiber 将渲染拆分为可中断的“工作单元”，允许 React 在渲染中插入优先级判断与让步（yield），从而改善动画、手势响应以及并发特性（concurrent rendering）的实现基础。

**面试要点**：说明两个阶段（reconciliation 与 commit），Fiber 如何让中断与恢复成为可能，以及它对 `useTransition`/并发渲染等特性的支撑作用。[LogRocket Blog+1](https://blog.logrocket.com/deep-dive-react-fiber/?utm_source=chatgpt.com)

**简化示意代码**（伪代码展示任务分片）：

```jsx
// 伪：把大任务拆成小任务，模拟 Fiber 调度
function workLoop(workQueue) {
  while (workQueue.length > 0 && !shouldYield()) {
    const unit = workQueue.shift();
    performUnitOfWork(unit);
  }
  if (workQueue.length > 0) {
    requestIdleCallback(() => workLoop(workQueue));
  } else {
    commitAll();
  }
}
```

### React 中的 `key` 属性有什么作用？

**解释**：`key` 用于在列表渲染时帮助 React 辨认元素的身份，从而在做 diff 时能正确地保留/复用 DOM 节点并最小化变更。合适的 key（稳定且唯一，如数据库 id）避免了错误的节点复用（例如输入框光标跳动、组件内部 state 错位）。不推荐使用索引作为 key（在列表会发生插入/删除/排序时会破坏复用）。

**代码 demo**：

```jsx
function TodoList({ items }) {
  return (
    <ul>
      {items.map(item => (
      <li key={item.id}>
        <input defaultValue={item.text} />
      </li>
    ))}
    </ul>
  );
}
```

### React 的事件机制和合成事件是如何工作的？

**解释**：React 使用“合成事件”（SyntheticEvent）作为跨浏览器的事件封装层。React 在根节点上绑定少量真实 DOM 事件（事件委托），当事件发生时，React 会构建合成事件对象并按组件树执行回调。合成事件的好处：统一跨浏览器行为、事件池（老版本）以复用对象、以及更简单的生命周期一致性（在某些场景下 React 能更好地控制事件顺序）。注意：事件处理器中的 `event.persist()` 可阻止事件被复用（旧机制），React 现在已改进事件对象生命周期但概念仍然一致。

**代码 demo**：

```jsx
function MyButton() {
  function handleClick(e) {
    console.log('target', e.target);
    // e is a SyntheticEvent
  }
  return <button onClick={handleClick}>Click</button>;
}
```

### 受控组件（controlled）和非受控组件（uncontrolled）有什么区别？

**解释**：

- **受控组件**：表单元素的值由 React state 完全控制（`value` + `onChange`）。好处：单一真相、便于验证、条件禁用、联动等。
- **非受控组件**：表单元素自己管理内部 state，通过 ref 在需要时读取值（类似传统 DOM 表单）。适用于简单场景或需要与第三方库集成且不想每次输入都触发渲染。

**代码 demo（对比）**：

```jsx
// 受控
function Controlled() {
  const [v, setV] = React.useState('');
  return <input value={v} onChange={e => setV(e.target.value)} />;
}

// 非受控
function Uncontrolled() {
  const ref = React.useRef();
  function submit() {
    alert(ref.current.value);
  }
  return <>
    <input ref={ref} defaultValue="init" />
    <button onClick={submit}>Submit</button>
  </>;
}
```

### 为什么在 React 中“组合优于继承”？

**解释**：组合（composition）更灵活、可组合、符合函数式思想；通过把小组件组合起来可以构建复杂功能，而继承往往造成紧耦合、难以复用、难以理解的类层级。React 的 props / children / render-props / hooks 都是组合的体现。官方建议使用组合来复用组件逻辑（而不是继承）。

**代码 demo（组合示例）**：

```jsx
function Modal({children}) {
  return <div className="modal">{children}</div>;
}

function App() {
  return (
    <Modal>
      <h1>Title</h1>
      <p>Content</p>
    </Modal>
  );
}
```

### React 的严格模式（`StrictMode`）有什么作用？

**解释**：`<StrictMode>` 在开发模式下启用额外检查与警告（并不影响生产构建），如：

- 检查过时的生命周期方法；
- 在开发环境对组件 mount/unmount 进行双重调用以暴露副作用中的不纯代码（例如不正确的清理）；
- 启用对某些未来改动的警告（如对 refs 的变化等）。

**面试要点**：说明 StrictMode 通过“在开发环境下执行额外运行”帮你发现副作用和不安全的生命周期。

**代码 demo**：

```jsx
ReactDOM.createRoot(root).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

### `useState` 的更新是异步的吗？

**解释**：在 React 中调用 `setState`（或 `setX`）**不会立即更新**当前渲染中的变量——它会安排一次重新渲染，更新会在随后渲染周期反映出来。并且 React 会对同一事件循环中的多个更新进行**批处理**（React 18+ 的自动批处理扩展了批处理的情形）。因此从调用 `setState` 到 DOM 更新是异步的（对当前渲染而言）；但如果你在同一个渲染里多次调用 `setState`，React 可能会合并这些更新（尤其在使用函数式更新时行为确定）。

**代码 demo（说明）**：

```jsx
function Demo() {
  const [n, setN] = React.useState(0);
  function onClick() {
    setN(n + 1);
    console.log(n); // 仍然是旧值（在当前渲染里）
    setN(n + 2);
  }
  return <button onClick={onClick}>{n}</button>;
}
```

关于自动批处理（React 18）的工作方式可参考官方发布说明。[react.dev](https://react.dev/blog/2022/03/29/react-v18?utm_source=chatgpt.com)

### 使用 `useState` 的函数式更新方式能带来哪些好处？

**解释**：函数式更新（`setState(prev => newVal)`）的优势：

- 保证在多次更新（尤其依赖旧值）时的正确性，避免闭包捕获旧 state 导致竞态；
- 在批处理或异步回调中能安全地基于最新值做计算；
- 更容易避免依赖数组问题。

**代码 demo**：

```jsx
function Counter() {
  const [count, setCount] = React.useState(0);
  function incTwice() {
    // 正确：每次都基于最新值
    setCount(c => c + 1);
    setCount(c => c + 1);
  }
  return <button onClick={incTwice}>{count}</button>;
}
```

### “状态提升”这种模式有哪些优缺点？

**解释**：

- 优点：把共享 state 放到最近的共同祖先，能保证单一数据源、易于同步与逻辑集中，便于验证与持久化。
- 缺点：会导致“祖先”组件变大（prop drilling），频繁更新可能导致更多子组件重渲染，降低局部封装性。需要配合 `memo`、context、或拆分组件来缓解。

**代码 demo（状态提升）**：

```jsx
function Parent() {
  const [value, setValue] = React.useState('');
  return (
    <>
      <Input value={value} onChange={v => setValue(v)} />
      <Preview value={value} />
    </>
  );
}
```

**缓解方案**：用 context、或者把不变部分 memo 化，或使用局部状态与事件回传。

### `useReducer` 相比 `useState` 优势体现在哪？

**解释**：

- 更适合描述复杂状态逻辑（多子值，基于操作类型的更新），便于把更新逻辑集中到 reducer 函数；
- 便于在多个组件之间共享或抽离状态逻辑（将 reducer 提取为纯函数）；
- 与中间件 / 日志 / 时间旅行（像 Redux）思想契合。

**代码 demo（计数器）**：

```jsx
function reducer(state, action) {
  switch (action.type) {
    case 'inc': return {count: state.count + 1};
    case 'dec': return {count: state.count - 1};
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, {count: 0});
  return (
    <>
      <button onClick={() => dispatch({type: 'dec'})}>-</button>
      {state.count}
      <button onClick={() => dispatch({type: 'inc'})}>+</button>
    </>
  );
}
```

### 如何优化因 `useContext` 引起的性能问题？

**解释**：Context 更新会触发所有消费该 context 的组件重新渲染（即使它们读取的值未改变）。优化策略：

- 将 context 值拆分为更粒度的 context（避免频繁更新的大对象）；
- 在提供方使用 `useMemo` 或稳定引用（避免每次 render 提供新对象）；
- 消费方用 `memo` 并把 context 中仅必要字段传入子组件，或使用选择器/订阅模式（像 Zustand 的订阅）；
- 把高频更新（如动画、光标位置）移出 React state，使用 ref 或外部 store。

**代码 demo（useMemo 防止不必要重渲染）**：

```jsx
function Provider({children}) {
  const [count, setCount] = React.useState(0);
  const value = React.useMemo(() => ({count, increment: () => setCount(c => c + 1)}), [count]);
  return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}
```

### 说一下 Redux 的核心思想

**解释**：Redux 的核心思想：

1. **单一源（single source of truth）**：整个应用的 state 存在一个 store；
2. **状态只读**：不能直接修改 state，只能派发 action；
3. **使用纯函数 reducer 描述 state 如何更新**：reducer 接收旧 state 与 action，返回新 state（纯函数）。

Redux 强调可预测性、时间旅行（devtools）、中间件链（处理异步/副作用）与易于测试。

**代码 demo（基本 Redux 概览，使用** `**@reduxjs/toolkit**` **推荐）**：

```jsx
// 使用 Redux Toolkit 简化
import { configureStore, createSlice } from '@reduxjs/toolkit';

const counter = createSlice({
  name: 'counter',
  initialState: {value: 0},
  reducers: {
    inc: s => { s.value += 1; },
    dec: s => { s.value -= 1; }
  }
});

const store = configureStore({ reducer: { counter: counter.reducer } });
store.dispatch(counter.actions.inc());
console.log(store.getState());
```

### 为什么说 Immer.js 在现代 Redux 生态中扮演重要角色？

**解释**：Immer 允许用“可变风格”的代码写 reducer（直接修改 draft），在内部会生成不可变的新 state（通过 structural sharing）。这样兼顾了编程便利性（直观的赋值语句）与不可变数据带来的性能/可预测性优势。Redux Toolkit 将 Immer 集成到 `createSlice`，简化 reducers 的编写。

**代码 demo（使用 Immer 的写法 vs 等价不可变写法）**：

```jsx
// Immer 风格（Redux Toolkit 已内置）
function reducer(state = {a: 0}, action) {
  if (action.type === 'inc') {
    state.a += 1; // 看起来是可变，但 Immer 会转成不可变更新
  }
  return state;
}

// 等价手写不可变写法
function reducerPure(state = {a: 0}, action) {
  if (action.type === 'inc') {
    return {...state, a: state.a + 1};
  }
  return state;
}
```

### `Redux` 的中间件（`Middleware`）机制是如何工作的？

**解释**：中间件是 Redux dispatch 流程中的拦截器。它接收 `store`（`getState`/`dispatch`）并返回一个接收 `next` 的函数，然后返回接收 `action` 的函数。中间件可以在 action 到达 reducer 之前拦截、修改、延迟或触发其他副作用（例如 thunk、saga、logger）。中间件以链式方式组合，最终调用 reducer。

**代码 demo（自定义 logger 中间件）**：

```jsx
const logger = store => next => action => {
  console.log('dispatching', action);
  const result = next(action);
  console.log('next state', store.getState());
  return result;
};

const store = Redux.createStore(rootReducer, Redux.applyMiddleware(logger));
```

### 你是否了解像 Zustand、Jotai 这类新兴的状态管理库？它们各自有什么特点？

**解释 & 要点**：

- **Zustand**：轻量、基于订阅的全局 store（没有 Provider 必要），使用简单的 API（`create`），组件可以选择性订阅切片状态，性能友好（按订阅字段更新）。适合替代 Redux 的简单场景。
- **Jotai**：基于原子（atom）概念的小粒度可组合状态，易于分片和复用，支持异步/派生状态。它把状态拆成原子，组件订阅原子，减少不必要渲染。
  两者都追求更简单更小的 API 曲线，并利用现代 React 特性来减少样板。

**代码 demo（Zustand 简单示例）**：

```jsx
import create from 'zustand';

const useStore = create(set => ({
  count: 0,
  inc: () => set(s => ({count: s.count + 1}))
}));

function Counter() {
  const {count, inc} = useStore(s => ({count: s.count, inc: s.inc}));
  return <button onClick={inc}>{count}</button>;
}
```

### 像 SWR 或 React Query 这样的库，它们主要解决了什么问题？

**解释**：这些库专注于**客户端数据获取（fetching）与缓存**，解决了常见问题：

- 请求缓存与去重（避免重复请求）；
- 后台刷新（stale-while-revalidate）策略；
- 请求失败重试、分页/无限滚动、请求状态（isLoading/isError）管理；
- 与 React 渲染整合（避免竞态、自动重试/刷新）；
- 方便的缓存失效（invalidations）与查询依赖管理。

它们把数据获取变成可声明的资源，简化了从服务器数据到 UI 的管线。

**代码 demo（React Query）**：

```jsx
import { useQuery } from '@tanstack/react-query';
function Todos() {
  const { data, isLoading } = useQuery(['todos'], () => fetch('/api/todos').then(r => r.json()));
  if (isLoading) return 'loading';
  return <ul>{data.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}
```

### 你是如何理解 Signals 的？它与 React 现有的状态管理方式有何不同？

**解释**：Signals（常见于 Solid、Angular signals、或 React 围绕 signal 的提议）是对可观察值的轻量封装，读取时会建立依赖，值变化会同步触发对应读取位置的更新。与 React 的 Hooks（基于组件重新渲染）不同，signals 更像细粒度的可变引用：改变 signal 只会更新真正依赖它的地方，而不需要整个组件重新渲染。优点：更小的更新粒度、潜在更少的重渲染、并且更接近响应式编程模型。React 社区对此也在探索（例如部分库/提案），但 React 的核心目前仍以 hooks & rerender 为主流。

**代码 demo（Signal 风格伪代码）**：

```jsx
// 伪示意（非官方 API）
const count = signal(0);
effect(() => {
  console.log('count changed', count.value);
});
count.value = 1; // 只触发依赖该 signal 的 effect
```

### 你能详细解释一下 React Hooks 的执行顺序和依赖规则吗？

**解释（关键规则）**：

1. **Hooks 必须在 React 函数组件或自定义 Hook 的顶层调用**，不能在条件、循环或嵌套函数中调用（保证每次渲染 Hook 调用顺序一致）。
2. React 根据调用顺序（索引）把 Hook 调用与内部状态槽（hook slots）一一对应，因此不能更改调用顺序。
3. **依赖规则**（针对 `useEffect` / `useMemo` / `useCallback`）：传入的依赖数组应列出所有在回调中使用到的外部变量（除非这些变量是稳定的，如 `ref.current`、常量或经 `useRef`/`useReducer` 管理的值），否则可能导致过时的数据或副作用问题。

**面试要点**：讲清楚“为什么”不能在条件中调用（因为索引错位会把 state/副作用错配），说明如何处理函数依赖（`useCallback`）、避免无限循环（不要把 setState 作为依赖，或使用函数式更新）。

**代码 demo（错误示范与修正）**：

```jsx
// 错误：Hook 在条件里
function Bad({ flag }) {
  if (flag) {
    React.useEffect(() => { /* ... */ }, []); // 不可行
  }
}

// 正确
function Good({ flag }) {
  React.useEffect(() => {
    if (!flag) return;
    // safe
  }, [flag]);
}
```

### `useEffect` 的执行时机具体是什么时候？它和 `useLayoutEffect` 有什么核心区别？

**解释**：

- `useEffect`：在浏览器完成布局与绘制之后异步执行（commit 后、paint 后），不会阻塞渲染，因此适合网络请求、订阅、日志等副作用。
- `useLayoutEffect`：在 DOM 变更提交但在浏览器绘制之前同步执行（在 commit 阶段），会阻塞浏览器绘制，适合需要同步读取/修改 DOM（例如测量元素尺寸并同步设置样式）以避免视觉闪烁。

**面试要点**：尽量用 `useEffect`；仅当你必须在绘制前同步读写 DOM 时才用 `useLayoutEffect`。

**代码 demo**：

```jsx
function Measure() {
  const ref = React.useRef();
  React.useLayoutEffect(() => {
    const rect = ref.current.getBoundingClientRect();
    // 这里同步读布局并可同步修改 DOM
  }, []);
  return <div ref={ref}>box</div>;
}
```

### `useEffect` 依赖项有什么用？

**解释**：依赖数组让 React 决定何时重新运行 effect：当数组中的任一引用（===）变化时，effect 会在下次 commit 后运行。它用于控制副作用的触发频率与条件，避免不必要的重复执行或遗漏必要更新。

**面试要点**：说明依赖应该包含 effect 中用到的所有外部变量；可以用 `useCallback`/`useMemo` 稳定引用；必要时明确说明 `// eslint-disable` 仅在确认安全的情况下使用。

**代码 demo**：

```jsx
React.useEffect(() => {
  // fetch whenever userId changes
  fetch(`/api/user/${userId}`).then(...);
}, [userId]);
```

### 在 `useEffect` 中，如何正确地处理异步请求并避免竞态条件（race condition）？

**解释 & 实践**：

- 在 effect 内使用一个**标记（isMounted / abort controller）**来忽略过时的响应；
- 使用 `AbortController` 取消 fetch 请求；
- 在有多个请求并发时，为每次请求生成唯一标识并只处理最新一次的结果；
- 如果 state 更新必须基于最新请求结果，使用 `useRef` 保存最新标识并在回调里比对。

**代码 demo（使用 AbortController）**：

```jsx
function User({ userId }) {
  const [data, setData] = React.useState(null);
  React.useEffect(() => {
    const ac = new AbortController();
    fetch(`/api/user/${userId}`, { signal: ac.signal })
      .then(r => r.json())
      .then(d => setData(d))
      .catch(err => {
        if (err.name === 'AbortError') return;
        throw err;
      });
    return () => ac.abort(); // cleanup cancels previous request
  }, [userId]);
  return <div>{JSON.stringify(data)}</div>;
}
```

### 什么时候应该使用 `useCallback` 和 `useMemo`？如果滥用它们会带来什么后果？

**解释**：

- 使用场景：当你需要**稳定的引用**（函数或计算结果）以避免子组件不必要重渲或避免重新创建昂贵计算时使用（例如传给依赖数组或作为 prop 给 `memo` 组件）。
- 滥用后果：增加复杂性与内存开销（缓存本身也有成本），且在很多情况下 `useMemo`/`useCallback` 的维护成本比它带来的性能提升要高（小组件/快速计算下反而更慢）。

**面试要点**：先测（profile），只在确实存在性能问题或避免不必要重新渲染时使用。

**代码 demo**：

```jsx
const memoizedFn = React.useCallback(() => {
  doSomething(expensiveParam);
}, [expensiveParam]);

const memoizedValue = React.useMemo(() => heavyCompute(x), [x]);
```

### `useRef` 有哪些常见的应用场景？

**解释 & 场景**：

- 访问 DOM 节点（`ref` 指向元素）；
- 存放跨渲染周期的可变值（例如计时器 id、上一次 props 值），且更新不会触发重渲染；
- 在回调中获取最新的值（避免闭包捕获旧值）；
- 实现非受控组件或与第三方库集成。

**代码 demo（保存上一次值）**：

```jsx
function Component({ value }) {
  const prevRef = React.useRef();
  React.useEffect(() => { prevRef.current = value; }, [value]);
  const prev = prevRef.current;
  return <div>prev: {String(prev)}</div>;
}
```

### `forwardRef` 和 `useImperativeHandle` 怎么用？

**解释**：

- `forwardRef` 用来把父组件传下来的 ref 转发到子组件的内部 DOM 或值上；
- `useImperativeHandle` 在使用 `forwardRef` 时允许子组件暴露自定义的实例值（而不是默认的 DOM 节点），通过第二个参数返回要暴露的对象。注意：这种方式破坏封装性，应谨慎使用，仅在需要 imperative API（例如 focus）时使用。

**代码 demo（暴露 focus API）**：

```jsx
const Input = React.forwardRef((props, ref) => {
  const inputRef = React.useRef();
  React.useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus()
  }));
  return <input ref={inputRef} {...props} />;
});

function Parent() {
  const ref = React.useRef();
  return <button onClick={() => ref.current.focus()}><Input ref={ref} /></button>;
}
```

### `useId` 如何解决 SSR 场景下的 ID 冲突和可访问性问题？

**解释**：`useId` 生成稳定且在服务器/客户端一致的 id（避免 SSR 首次渲染时 id 不一致导致 hydration mismatch）。它用于生成与组件实例相关的唯一 id（例如 `label` 与 `input` 的 `id`/`htmlFor` 关系），改善可访问性和避免手写 id 的冲突。

**代码 demo**：

```jsx
function TextField({ label }) {
  const id = React.useId();
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  );
}
```

### `useTransition` 和 `useDeferredValue` 的使用和区别？

**解释**：

- `useTransition`：用于把某些更新标记为低优先级“transition”，提供 `startTransition` 来包裹更新并返回 `isPending` 标志。适合将“非关键”渲染放到后台（例如列表过滤后的渲染）。
- `useDeferredValue`：用于延迟某个值的更新，使 UI 在输入时保持响应（例如输入框即时反馈，但大列表过滤延迟更新）。它更适合在你不能轻易把更新包装到 `startTransition` 中时使用。

**代码 demo（对比）**：

```jsx
// useTransition
function Search() {
  const [q, setQ] = React.useState('');
  const [isPending, startTransition] = React.useTransition();
  const [results, setResults] = React.useState([]);

  function onChange(e) {
    const v = e.target.value;
    setQ(v);
    startTransition(() => {
      setResults(expensiveFilter(v));
    });
  }
  return <>
    <input value={q} onChange={onChange} />
    {isPending ? 'loading...' : <List items={results} />}
  </>;
}

// useDeferredValue
function Search2() {
  const [q, setQ] = React.useState('');
  const deferredQ = React.useDeferredValue(q);
  const results = React.useMemo(() => expensiveFilter(deferredQ), [deferredQ]);
  return <>
    <input value={q} onChange={e => setQ(e.target.value)} />
    <List items={results} />
  </>;
}
```

参见官方 `useTransition` 与 `useDeferredValue` 文档。[react.dev+1](https://react.dev/reference/react/useTransition?utm_source=chatgpt.com)

### React18 中的自动批处理（Automatic Batching）如何工作？

**解释**：React 18 引入了更广泛的自动批处理：若使用 `createRoot`，React 会把同一事件循环中的多个 `setState` 调用自动合并为一次重渲染（不仅限于 React 事件处理器，还包括 timeout、promise 回调等）。这能显著减少不必要渲染次数，提高性能。若需要立即刷新某个更新，可以用 `flushSync`（但应慎用）。

**代码 demo（示例）**：

```jsx
function Example() {
  const [a, setA] = React.useState(0);
  const [b, setB] = React.useState(0);
  function handleClick() {
    setA(v => v + 1);
    setB(v => v + 1);
    // 在 React 18+ 自动批处理下这会只触发一次渲染
  }
  return <button onClick={handleClick}>Click</button>;
}
```

官方说明关于 React 18 的自动批处理可以参考 React 官方发布说明。[react.dev](https://react.dev/blog/2022/03/29/react-v18?utm_source=chatgpt.com)

### React19 中的 `Activity` 组件与 `Suspense` 有什么区别？

**解释**（基于 React 19 官方描述）：`<Suspense>` 关注“数据/代码尚未就绪时显示 fallback”，它是基于“某个子树 suspend（抛出 promise）”的通用边界。`<Activity>` 是 React 19 里新增的用于“隐藏 UI 但保留组件状态”的工具（把部分 UI 暂时‘冻结’/‘睡眠’而不卸载状态），与 Suspense 的目的不同：Suspense 是等待数据就绪，Activity 更多是关于界面活动/状态保留与按需唤醒。官方文档对 Activity 的用途与示例有说明。[react.dev+1](https://react.dev/reference/react/Activity?utm_source=chatgpt.com)

**代码 demo（伪示意）**：

```jsx
function Tabs() {
  const [active, setActive] = React.useState('home');
  return (
    <>
      <nav>
        <button onClick={() => setActive('home')}>Home</button>
        <button onClick={() => setActive('settings')}>Settings</button>
      </nav>
      <Activity isActive={active === 'home'}>
        <HomePanel />
      </Activity>
      <Activity isActive={active === 'settings'}>
        <SettingsPanel />
      </Activity>
    </>
  );
}
// 注：上例为示意，具体 API 以官方文档为准。
```

### React19 中的 `use` Hook 如何简化异步数据处理？

**解释**：React 19 引入/标准化了 `use()`（或类似）用于直接在组件中读取 promise/异步资源：在服务端渲染或与 Suspense 结合时，`use()` 可以直接接受 Promise，并在渲染期处理等待逻辑，从而减少显式的 `useEffect`/loading state 管理。它使得组件可以更直接地“同步”读取异步数据（在并发渲染模型与 Suspense 配合下），简化代码结构。

**示意代码**：

```jsx
// 伪示例（以官方 use() 为准）
function Profile({ userId }) {
  const user = use(fetchUser(userId)); // 如果未就绪，组件会 suspend 到 Suspense
  return <div>{user.name}</div>;
}
```

参见对 `use()` 的文章与示例（社区/官方解读）。[Medium](https://medium.com/%40ademyalcin27/the-new-use-hook-in-react-19-a-game-changer-for-simpler-data-fetching-and-context-management-cc45cc5ebd28?utm_source=chatgpt.com)

### 说一下 React19 对表单处理的改进：`useActionState` 和 `useFormState`

**解释**：React 19 在表单与 actions 上引入了一系列 API（如 `useActionState`）来更好地描述服务器 action 的状态与表单交互：`useActionState` 帮助追踪 action 的当前状态、返回 action 可作为 `<form>` 的 action prop，以及提供 pending 标志以显示加载状态。这使得表单的提交状态管理与 UI 更加协同、也更易与 transitions 集成。官方参考文档对这些 Hook 有说明与示例。[react.dev+1](https://react.dev/reference/react/useActionState?utm_source=chatgpt.com)

**代码 demo（示意）**：

```jsx
function MyForm() {
  const [state, action, isPending] = useActionState(async data => {
    // action: 可以在服务端或路由处理器中执行
    const res = await fetch('/submit', { method: 'POST', body: JSON.stringify(data) });
    return res.json();
  }, {initialState: null});

  return (
    <form action={action}>
      <input name="name" />
      <button type="submit">{isPending ? 'Submitting...' : 'Submit'}</button>
    </form>
  );
}
```

### React19 中如何直接在组件中渲染 meta 标签和样式表？

**解释**：React 19 引入了对文档级 metadata 与 stylesheet 的更简单内联支持，允许在组件树中声明 `<title>`/`<meta>`/`<link rel="stylesheet">` 等并由 React 在 render 时把它们“提升”到 `<head>` 中（hoist）。这简化了 SSR/组件化页面对 meta 信息和样式加载的管理。社区与官方教程给出具体示例。[egghead.io+1](https://egghead.io/hoist-title-and-meta-tags-in-react-19~h6z5l?utm_source=chatgpt.com)

**代码 demo（示意）**：

```jsx
function Page() {
  return (
    <>
      <title>My Page</title>
      <meta name="description" content="..." />
      <link rel="stylesheet" href="/page.css" />
      <main>content</main>
    </>
  );
}
```

### React19 的 ref 作为 prop 传递有什么优势？

**解释**：React 19 开始允许在函数组件中直接使用 `ref` 作为 prop（不再必须 `forwardRef`），简化了常见场景下的 ref 转发，减少模板样板代码并提高可读性。未来版本会逐步淘汰强制 `forwardRef` 的样式（官方会提供 codemod 帮助迁移）。优点：更少的 boilerplate、更直接的 API、更易读的代码。[react.dev+1](https://react.dev/blog/2024/12/05/react-19?utm_source=chatgpt.com)

**代码 demo（React 19 风格）**：

```jsx
// React 19 允许直接接收 ref prop（示例  
function MyInput({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}

function Parent() {
  const r = React.useRef();
  return <MyInput ref={r} />;
}

// 说明：在老版本仍需 forwardRef；在 React 19 中该用法被支持。
```

## 各大互联网大厂工程化相关面试真题&答案

### 同一个页面三个组件请求同一个 API

**面试要点/答案要点**

- 避免重复请求：使用共享状态或请求去重（dedupe）。
- 可用方案：全局缓存（Redux/Pinia/Zustand）、请求级缓存（axios cache）、请求队列 + Promise 复用、SSR/预取。
- 细节：请求参数相同才复用，考虑缓存失效、并发冲突、错误重试、取消请求（AbortController）。

**示例代码（用 Promise 去重 + 简单缓存，适用于 fetch/axios）**

```javascript
// requestPool.js
const pool = new Map(); // key -> Promise
const cache = new Map(); // key -> { data, expire }

function genKey(url, params) {
  return url + '::' + JSON.stringify(params || {});
}

export async function request(url, params = {}, opts = {}) {
  const key = genKey(url, params);
  const now = Date.now();
  const ttl = opts.ttl || 0;

  if (cache.has(key)) {
    const { data, expire } = cache.get(key);
    if (!expire || expire > now) return data;
    cache.delete(key);
  }

  if (pool.has(key)) {
    // 有正在进行的请求，直接复用 Promise
    return pool.get(key);
  }

  const p = fetch(url, {
    method: opts.method || 'GET',
    headers: opts.headers,
    body: opts.body ? JSON.stringify(opts.body) : undefined,
    signal: opts.signal,
  })
    .then(res => {
      if (!res.ok) throw new Error('Network error');
      return res.json();
    })
    .then(data => {
      if (ttl > 0) cache.set(key, { data, expire: now + ttl });
      return data;
    })
    .finally(() => {
      pool.delete(key);
    });

  pool.set(key, p);
  return p;
}
// ExampleComponent.jsx (React)
import React, { useEffect, useState } from 'react';
import { request } from './requestPool';

function Item() {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ac = new AbortController();
    request('/api/items', {}, { ttl: 5000, signal: ac.signal })
      .then(setData)
      .catch(err => { if (err.name !== 'AbortError') console.error(err);});
    return () => ac.abort();
  }, []);
  return <div>{data ? JSON.stringify(data) : 'loading'}</div>;
}

export default function Page() {
  return (
    <div>
      <Item /><Item /><Item />
    </div>
  );
}
```

面试话术：*“我们采用请求复用（Promise pooling）+ 可配置 TTL 缓存，避免同一时刻的重复网络请求，同时支持 AbortController 取消与错误边界处理。”*

### cjs / esm / umd 等区别是什么

**面试要点**

- **CJS (CommonJS)**：Node.js 传统模块（`require/module.exports`），同步加载，适用于服务器。
- **ESM (ES Modules)**：`import/export`，静态分析、支持 tree-shaking、可以异步加载，浏览器/现代打包器优先。
- **UMD (Universal Module Definition)**：兼容 AMD、CJS、全局变量，常用于发布兼容包（CDN）。
- 选型原则：库发布 -> 提供 ESM + CJS + UMD（兼容旧环境）；应用中优先 ESM。

**示例：输出多格式（Rollup 配置）**

```javascript
// rollup.config.js
import commonjs from '@rollup/plugin-commonjs';
import nodeResolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';

export default [
  {
    input: 'src/index.js',
    output: { file: 'dist/bundle.cjs.js', format: 'cjs' },
    plugins: [nodeResolve(), commonjs()],
  },
  {
    input: 'src/index.js',
    output: { file: 'dist/bundle.esm.js', format: 'es' },
    plugins: [nodeResolve()],
  },
  {
    input: 'src/index.js',
    output: { file: 'dist/bundle.umd.js', format: 'umd', name: 'MyLib' },
    plugins: [nodeResolve(), commonjs(), terser()],
  },
];
```

面试话术：*“库通常提供 ESM 以便 tree-shaking，CJS 保持 Node 兼容，UMD 提供浏览器直接引入支持。”*

### 前端权限管理的模型

**要点**

- **权限维度**：认证（AuthN） vs 授权（AuthZ）。
- **模型**：

- - RBAC（基于角色 Role-Based）—— 角色映射权限集合（常见）。
  - ABAC（基于属性 Attribute-Based）—— 基于资源/用户/环境属性判断（更灵活）。
  - PBAC / ACL（基于策略或访问控制列表）。

- **前端实践**：最小权限展示、路由守卫、组件级权限指令、后端返回可授权菜单/按钮、策略中心化（策略服务）。
- **防御性设计**：前端只做 UI 隐藏，关键权限必须在后端校验。

**代码示例：简单 RBAC（React + route guard）**

```java
// auth.js
export const currentUser = { roles: ['editor'] };
export const hasPermission = (permission) => {
const rolePerm = {
    admin: ['read','write','delete'],
    editor: ['read','write'],
    guest: ['read']
    };
return currentUser.roles.some(r => (rolePerm[r] || []).includes(permission));
};
// ProtectedButton.jsx
import React from 'react';
import { hasPermission } from './auth';

export default function ProtectedButton({ permission, children, ...rest }) {
  if (!hasPermission(permission)) return null; // 或者disabled
  return <button {...rest}>{children}</button>;
}
```

面试话术：*“前端以 RBAC 做展示权限，后端做强校验；复杂场景可用 ABAC 实现细粒度策略。”*

### peerDependencies

**要点**

- `peerDependencies` 表示包需要宿主（项目）安装的依赖版本（通常用于插件/库与宿主共享同一份依赖，如 React）。
- 作用：避免库自带多份 React（避免版本冲突），让宿主控制依赖版本。
- 注意事项：发布时声明 peerDeps，配合 `peerDependenciesMeta` 标注可选项；在开发时 use `npm/yarn/pnpm` 的 `--legacy-peer-deps`/workspace 管理。

**示例 package.json**

```json
{
  "name": "my-component-lib",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "peerDependenciesMeta": {
    "react": { "optional": false }
  }
}
```

面试话术：*“peerDependencies 用在插件/组件库，确保宿主项目安装并统一控制依赖的版本，防止重复打包与运行时冲突。”*

### pnpm 有什么优势

**要点**

- 磁盘节省：内容寻址存储（store）+ 硬链接，避免重复依赖。
- 安装速度快、严格的 node_modules 布局（避免“幽灵依赖”），workspace 支持好，适合 monorepo。
- 对包管理策略（如 `pnpmfile`、`hooks`）与锁文件健壮。
- 面试可补充：需处理 Node resolution 的某些老工具兼容问题，但现代工具兼容良好。

**示例：pnpm workspace 配置**

```bash
// pnpm-workspace.yaml
packages:
- 'packages/*'
// package.json (root)
{
  "name": "monorepo",
  "private": true,
  "devDependencies": {}
}
```

面试话术：*“pnpm 在 monorepo 环境下能显著节省磁盘并提高安装速度，同时强制依赖声明，减少运行时问题。”*

### eslint 作用

**要点**

- 静态代码检查，保持代码风格一致、捕捉潜在错误（未使用变量、语法错误）。
- 支持自定义规则、插件（React、TypeScript、Security）。
- 在工程化中：作为 CI 门槛、pre-commit（husky + lint-staged）环节、IDE 即时反馈。

**示例：ESLint + Prettier 配置**

```javascript
// .eslintrc.js
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'react'],
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  rules: {
    'no-unused-vars': 'warn',
    'react/prop-types': 'off'
  },
};
```

面试话术：*“ESLint 是代码质量第一道防线，结合 CI 和 pre-commit 能有效控制代码回退成本。”*

### browserslist

**要点**

- `browserslist` 用来声明支持的浏览器范围，影响 Babel、Autoprefixer、PostCSS、webpack/bundler 的目标转译与 polyfill。
- 写法：`"> 1%, last 2 versions, not dead"` 或针对不同环境（production/development）。

**示例：package.json 中配置**

```json
{
  "browserslist": {
    "production": [">0.2%", "not dead", "not op_mini all"],
    "development": ["last 1 chrome version", "last 1 firefox version"]
  }
}
```

面试话术：*“通过 browserslist 明确目标环境，减小 polyfill 与转译体积，同时提升性能。”*

------

### minify 代码压缩

**要点**

- 目的是减小 JS/CSS 体积、提高首屏加载速度。通常使用 terser/uglify/esbuild/terser-webpack-plugin。
- 注意：不要混淆压缩与混淆，压缩保留逻辑，混淆改变变量名；需要处理 source map、console 去除、产物调试。

**示例（webpack production 使用 terser）**

```javascript
// webpack.config.prod.js (片段)
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({
      terserOptions: { compress: { drop_console: true } }
    })],
  },
};
```

面试话术：*“生产构建使用 terser/esbuild 来压缩，同时配置去掉 console 并保留 source map 以便排查。”*

### package-lock.json

**要点**

- `package-lock.json`（npm）锁定依赖树、保证安装一致性。`pnpm-lock.yaml` / `yarn.lock` 同理。
- 不应手动编辑，CI/CD 中应提交锁文件；在库项目中可有不同策略（应用必须提交，库可视发布策略）。

**示例检查命令**

```bash
# 查看被锁定版本
npm ci        # 保证使用 lockfile 安装，适合 CI
npm audit     # 安全审计
```

面试话术：*“锁文件保证环境可复现，CI 用* `*npm ci*` *强制严格安装。”*

### serverless

**要点**

- Serverless：按需计算（函数）平台（AWS Lambda、Vercel、Netlify、Cloud Functions），免运维，按调用付费。
- 适用场景：事件驱动、API 后端、轻量短时任务；不适合长时任务（需要注意冷启动、资源上限、并发限制）。
- 工程要点：无状态、快速启动、合理冷启动优化、并发与超时、监控与本地调试（serverless-offline）。

**示例：简单 AWS Lambda handler（Node）**

```javascript
// handler.js
exports.handler = async (event) => {
  const name = event.queryStringParameters?.name || 'world';
  return {
    statusCode: 200,
    body: JSON.stringify({ msg: `hello ${name}` }),
  };
};
```

**Vercel Serverless（api/hello.js）**

```javascript
// api/hello.js
export default function handler(req, res) {
  res.status(200).json({ hello: 'world' });
}
```

面试话术：*“Serverless 省运维成本，但需要设计无状态、短时任务和冷启动优化策略，CI/CD 与本地模拟是关键。”*

### 常见的 Loader（你用过哪些）

**要点**

- Webpack Loader 用于对模块进行转换：`babel-loader`, `ts-loader`, `css-loader`, `style-loader`, `sass-loader`, `file-loader`（v5 推荐 asset modules）, `url-loader`（已废弃），`vue-loader`。
- 使用场景举例：把 TS 转为 JS，编译 SASS，处理图片、字体。

**示例 webpack loader 配置**

```javascript
module.exports = {
  module: {
    rules: [
      { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
      { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.(png|jpg|svg)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 8192 } } }
    ]
  }
};
```

面试话术：*“Loader 是文件到模块的转换器，我常用 babel-loader/ts-loader/vue-loader/css-loader/sass-loader 处理编译与资源。”*

### 常见的 Plugin（见过哪些）

**要点**

- Webpack 插件用于扩展构建流程：`HtmlWebpackPlugin`, `MiniCssExtractPlugin`, `DefinePlugin`, `HotModuleReplacementPlugin`, `TerserPlugin`, `CopyWebpackPlugin`, `ForkTsCheckerWebpackPlugin`。
- 插件职责更广：影响构建生命周期、产物、输出等。

**示例（常见插件配置）**

```javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({ template: './index.html' }),
    new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' }),
    new (require('webpack')).DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) })
  ]
};
```

面试话术：*“Plugin 能干预打包生命周期，常见插件用于 HTML 注入、提取 CSS、并行类型校验、代码压缩等。”*

### Loader 和 Plugin 的区别

**要点**

- **Loader**：用于把文件转换为模块（链式、针对单个模块），例如把 SASS -> CSS -> JS。
- **Plugin**：扩展打包器生命周期（全局影响），可以处理多个文件、控制输出、做优化、注入行为。
- 面试总结句子：*Loader = 文件级转换，Plugin = 构建流程级别扩展。*

**简要示例**：想把 `.foo` 文件转换为 JS 用 Loader；想在每次构建后生成报告用 Plugin。

### Webpack 构建流程简单说一下

**要点：六大阶段**

1. 解析配置（Configuration）
2. 初始化 Compiler（Compiler）并注册插件
3. 从 entry 开始构建模块依赖图（Compilation）
4. 对模块应用 loader 转换，收集依赖
5. 进行优化（tree-shaking、code splitting、minify）
6. 输出 assets（emit）

- 插件可以 tap 到这些钩子影响流程。

**面试话术**：*“Webpack 从配置出发创建 Compiler，遍历入口文件构建依赖图、对模块应用 Loader、使用 Plugin 做优化与打包，最后 emit 产物。”*

### 使用 webpack 开发时可以提高效率的插件

**常用插件**

- `HtmlWebpackPlugin`（自动注入）
- `ForkTsCheckerWebpackPlugin`（TS 类型校验异步）
- `HotModuleReplacementPlugin`（HMR）
- `webpack-dev-server` / `webpack-dev-middleware`（本地开发）
- `eslint-webpack-plugin`（开发时即时 lint）
- `SpeedMeasurePlugin`（构建性能分析）

**示例：dev config**

```javascript
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  plugins: [
    new ForkTsCheckerWebpackPlugin(),
    new (require('webpack')).HotModuleReplacementPlugin()
  ],
  devServer: { hot: true, port: 3000 }
};
```

面试话术：*“结合 HMR、异步类型检查和 lint 插件可以保持开发流畅且保证质量。”*

### 文件指纹是什么？怎么用？

**要点**

- 文件指纹（hash）用于静态资源长缓存（Cache busting）。常见：`[contenthash]`、`[chunkhash]`、`[hash]`。
- 实践：生成带 hash 的文件名并在 HTML 中引用，CDN 缓存策略使用长缓存，变更时更新文件名。

**示例 webpack output**

```javascript
output: {
  filename: '[name].[contenthash:8].js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]'
}
```

面试话术：*“用 contenthash 保证只在内容变化时文件名变化，配合缓存策略大幅提升前端性能。”*

### 如何优化 Webpack 构建速度？

**要点（实战清单）**

- 使用 `cache`（filesystem cache）
- 使用 `thread-loader` / `esbuild-loader` 或 `swc-loader` 替代 babel 在非复杂转译下加速
- 开启 `parallel`（terser 的并行）
- 使用 `include/exclude` 精准匹配 loader 范围
- 按需编译（DLL、模块联邦、持久化缓存）
- 使用 `source-map` 选择（开发 cheaper map）
- 减少 polyfills、合理拆分 vendor

**示例：开启 filesystem cache 与 esbuild-loader**

```javascript
module.exports = {
  cache: { type: 'filesystem', buildDependencies: { config: [__filename] } },
  module: {
    rules: [
      { test: /\.tsx?$/, loader: 'esbuild-loader', options: { loader: 'tsx', target: 'es2017' } }
    ]
  }
};
```

面试话术：*“优先开启 filesystem cache 和用更快的编译器（esbuild/swc），再结合按需 loader 配置与并行化即可显著降构建时间。”*

### 写过 Loader 吗？缩写 loader 思路

**要点**

- Loader 是一个导出函数：接收源内容，返回转换后的内容（或异步回调）。
- 思路：解析输入 -> 进行 transform（babel transform / regex / parse AST）-> 返回 JS module（module.exports / export default）。
- 注意 sourceMap 支持、异步处理、cacheable 声明。

**示例：一个简单的 loader（把** `**// @upper**` **标记的字符串转为大写）**

```javascript
// loaders/upper-loader.js
module.exports = function (source, map) {
  this.cacheable && this.cacheable();
  const result = source.replace(/\/\/\s*@upper\n([\s\S]*?)\n\/\/\s*@end/gi, (_, txt) => txt.toUpperCase());
  this.callback(null, result, map);
};
// webpack config
module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: [{ loader: require.resolve('./loaders/upper-loader') }] }]
  }
};
```

面试话术：*“写 loader 的关键是处理输入输出与 sourceMap，通常结合 AST（babel/parser）进行复杂转换。”*

------

### 写过 Plugin 吗？编写 Plugin 思路

**要点**

- Plugin 是一个类/函数，`apply(compiler)` 内注册钩子（compiler.hooks）。
- 思路：识别希望插手的阶段（比如 compilation, emit），使用同步/异步 tap 方法，并处理 assets 或信息。

**示例：简单插件 — 在 emit 时注入 banner 文件**

```javascript
// plugins/banner-plugin.js
class BannerPlugin {
  constructor(options = {}) { this.options = options; }
  apply(compiler) {
    compiler.hooks.emit.tapAsync('BannerPlugin', (compilation, callback) => {
      const banner = this.options.banner || 'Built by BannerPlugin';
      compilation.assets['banner.txt'] = {
        source: () => banner,
        size: () => banner.length
      };
      callback();
    });
  }
}
module.exports = BannerPlugin;
// webpack.config.js
const BannerPlugin = require('./plugins/banner-plugin');
module.exports = { plugins: [new BannerPlugin({ banner: 'Hello from build' })] };
```

面试话术：*“Plugin 本质是订阅 webpack 钩子，能全局影响流程，适合跨模块或构建产物级别的操作。”*

### Babel 原理

**要点**

- Babel 工作流程：parse -> AST -> transform（plugin/transpiler，使用 visitor 修改 AST）-> generate（输出代码 & sourceMap）。
- 插件结构：visitor（节点访问器）对 AST 节点进行替换/插入，preset 是一组插件的集合。
- Babel 的目标：把现代 JS 转为目标环境兼容的 JS（polyfill 与 transform 分开）。

**示例：自定义 Babel plugin（把** `**log('x')**` **->** `**console.log('x')**`**）**

```javascript
// babel-plugin-to-console.js
module.exports = function ({ types: t }) {
  return {
    visitor: {
      CallExpression(path) {
        if (t.isIdentifier(path.node.callee, { name: 'log' })) {
          path.node.callee = t.memberExpression(t.identifier('console'), t.identifier('log'));
        }
      }
    }
  };
};
```

使用：

```javascript
// .babelrc
{ "plugins": ["./babel-plugin-to-console.js"] }
```

面试话术：*“Babel 是 AST 转换器，插件通过访问器模式在 AST 上做变换，最后再生成代码与 sourcemap。”*

### source map 是什么？生产环境怎么用？

**要点**

- source map 映射编译/压缩后的代码到源代码，便于调试。
- 生产使用策略：

- - 选用合适的映射类型（`source-map` / `hidden-source-map` / `nosources-source-map`）。
  - 若担心源码泄露，使用 `hidden-source-map` 并只在 Sentry/错误收集工具上上传 map，不在浏览器可直接访问。
  - 注意：公开 source map 可能泄露源码或注入安全风险。

**示例（webpack）**

```javascript
// dev
devtool: 'eval-cheap-module-source-map'
// prod (不公开，但上传到错误收集)
devtool: 'hidden-source-map'
```

面试话术：*“生产可使用 hidden-source-map 上传到错误监控（Sentry），避免直接暴露源码同时保留可追踪性。”*

### 文件监听原理

**要点**

- 两种方式：**轮询(polling)** 与 **文件系统事件**（inotify on Linux, fsevents on macOS, ReadDirectoryChangesW on Windows）。
- Node.js 使用 `fs.watch` 或 `fs.watchFile`（后者是轮询），工具如 webpack-dev-server / chokidar 使用更稳定的跨平台库 chokidar。
- 面试补充：大量文件时轮询耗资源，inotify 有文件数上限 (可调整 `fs.inotify.max_user_watches`)。

**示例（chokidar）**

```javascript
const chokidar = require('chokidar');
const watcher = chokidar.watch('./src', { ignored: /node_modules/ });
watcher.on('change', path => console.log('changed', path));
```

面试话术：*“一般用 chokidar 做跨平台监听，优先使用文件事件，轮询仅作 fallback 或 CI 环境下使用。”*

### Webpack 热更新（HMR）原理

**要点**

- HMR：在不刷新页面的情况下替换、添加或删除模块。
- 流程：Dev server 通过 websocket 通知客户端有更新 -> 客户端获取更新模块 -> 运行模块的 accept 回调（module.hot.accept） -> 局部替换模块并执行更新逻辑 -> 若不能处理则触发全页 reload。
- HMR 能保持应用状态（在组件层面实现），适合 UI 开发。

**示例（React + webpack HMR）**

```jsx
// index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

if (module.hot) {
  module.hot.accept('./App', () => {
    const NextApp = require('./App').default;
    root.render(<NextApp />);
  });
}
```

面试话术：*“HMR 将模块热替换并调用模块的 accept 回调，能保留应用内状态，提升开发效率。”*

### Webpack 事件机制了解吗？

**要点**

- Webpack 使用 Tapable（类似事件/钩子系统），提供同步/异步钩子（tap/tapAsync/tapPromise）。
- 常见钩子：`compiler.hooks.beforeRun`, `compiler.hooks.emit`, `compilation.hooks.optimizeChunks` 等。
- Plugin 利用这些钩子改变构建流程或产物。

**示例（插件内 tap）**

```javascript
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, cb) => {
  // 修改 compilation.assets
  cb();
});
```

面试话术：*“Webpack 钩子基于 Tapable，Plugin 通过 tap 注册不同阶段的逻辑。”*

### 了解 Webpack5 吗，相比 Webpack4 有哪些提升？

**要点**

- 主要提升：

- - 更好的持久化 cache（filesystem cache）提升构建速度。
  - 内置 Module Federation（实验/插件生态），改善微前端。
  - 更现代的 asset modules（替代 file/url loaders）。
  - 更严格的默认性能/Tree shaking 改进。
  - 更小的 runtime 与优化机制。

- 面试话术：*“Webpack5 将性能与模块共享作为核心改进，尤其是持久化 cache 与 Module Federation 对大型项目受益显著。”*

### 模块联邦（Module Federation）理解

**要点**

- Module Federation：Webpack5 特性，允许在运行时动态加载并共享多个独立构建的模块（micro-frontends）。
- 优点：独立部署、共享 runtime/库，按需加载远程模块。
- 注意事项：共享依赖版本协调、跨域/运行时加载策略、安全（验证远端模块来源）。

**示例（host & remote 简要）**

```javascript
// remote webpack config
new ModuleFederationPlugin({
  name: 'remoteApp',
  filename: 'remoteEntry.js',
  exposes: { './Button': './src/Button' },
  shared: { react: { singleton: true, eager: true }, 'react-dom': { singleton: true } }
});
// host webpack config
new ModuleFederationPlugin({
  name: 'host',
  remotes: { remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js' },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
});
// host usage
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
```

面试话术：*“Module Federation 支持运行时共享模块，便于团队独立部署子应用并共享核心库。”*

### 什么是 Webpack？它的作用是什么？

**要点**

- Webpack 是模块打包器（module bundler），从入口构建依赖图并输出静态资源（JS/CSS/图片）。
- 作用：模块化、资源处理（loader）、构建优化（plugin）、代码分割、tree-shaking、生产构建。

面试话术：*“Webpack 把前端项目的各种资源当模块来处理并生成优化后的静态资源，以支持现代前端开发流程。”*

------

### Webpack 中有哪些核心概念？

**要点列表**

- Entry：应用入口
- Output：构建输出
- Module & Rules（Loader）：模块及如何转换它
- Plugins：扩展构建流程
- Chunk：按需加载单元
- Asset：静态资源
- Dependency Graph：依赖图
- Mode：production/development

面试话术：*“理解 entry/module/plugin/chunk/output 这些基本概念能帮助掌握 webpack 的整体构建模型。”*

### Webpack 的 mode 是什么

**要点**

- `mode` = `'development' | 'production' | 'none'`，配置默认优化项（如 uglify、tree shaking）。
- `development`：更快构建、保留调试信息；`production`：启用优化、压缩、性能优化。

**示例**

```javascript
module.exports = { mode: process.env.NODE_ENV || 'development' };
```

面试话术：*“mode 决定内置优化集合，CI 中请显式指定 production 以获得体积与性能优化。”*

------

### 什么是代码分割（Code Splitting）？如何在 Webpack 中实现？

**要点**

- 代码分割：将包拆成多个 bundle，按需加载（减少首屏体积）。
- 实现方式：

- - 静态：entry 分割（multiple entries）
  - 动态：`import()`（动态 import）实现懒加载
  - CommonsChunk / SplitChunksPlugin（提取 vendor）

- 场景：路由懒加载、按需组件、按功能拆包。

**示例：React 路由懒加载 + webpack splitChunks**

```jsx
// App.jsx
import React, { Suspense } from 'react';
const Home = React.lazy(() => import('./Home'));
const Admin = React.lazy(() => import('./Admin'));

function App(){
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {/* 假设基于路由 */}
      <Home />
      <Admin />
    </Suspense>
  );
}
export default App;
// webpack optimization
optimization: {
  splitChunks: {
    chunks: 'all',
      cacheGroups: {
      vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' }
    }
  }
}
```

面试话术：*“首屏加载最小化、路由/组件按需加载和 vendor 提取是代码分割的三大常见策略。”*

## 各大互联网大厂性能优化相关面试真题&答案

### 性能优化之项目初始化

**说明 / 关键点**

- 项目初始化决定了运行时 & 打包基线：选择构建工具（Vite/webpack/Rollup）、模块系统（ESM/CJS）、语言（TS）、按需加载策略、第三方库替换（例如 lodash -> lodash-es / dayjs 替代 moment）。
- 约定：开启 ESModule、tree-shakeable 包、按需引入 UI 库、开 dev/prod config 分离。
- 初始工程需包含性能可观测点（Lighthouse、Sourcemap、bundle analyzer）及 CI 流程检查体积阈值。

**建议步骤**

1. 选择 Vite（开发启动快）或 modern webpack（如果需要复杂插件）。
2. 使用 ESM 包与 `sideEffects` 在 package.json。
3. 初始引入 bundle 分析、HMR、缓存策略（esbuild / terser）、压缩。
4. 将可选大型依赖列为外部（CDN/动态加载）。
5. 配置 CI 下的体积检测（比如 `size-limit`）。

**代码 demo（Vite + React + TypeScript）**

```js
package.json
{
  "name": "perf-starter",
  "version": "0.1.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview",
    "analyze": "vite build --watch --mode analyze"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "vite": "^5.0.0",
    "typescript": "^5.0.0",
    "esbuild": "^0.18.0",
    "rollup-plugin-visualizer": "^5.5.0"
  }
}
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(({ mode }) => ({
  plugins: [
    react(),
    // 在 analyze 模式下开启可视化
    ...(mode === 'analyze' ? [visualizer({ open: true, gzipSize: true })] : [])
  ],
  build: {
    target: 'es2018',
    sourcemap: mode === 'development',
    // chunkSizeWarningLimit 可调整
    chunkSizeWarningLimit: 1000,
    rollupOptions: {
      output: {
        manualChunks: {
          // 初始公共chunk示例
          vendor: ['react', 'react-dom']
        }
      }
    }
  }
}));
```

示例 CI 检查（`size-limit`）:

```bash
npm i -D size-limit @size-limit/preset-small-lib
```

`package.json` 加入：

```json
"size-limit": [
  {
    "path": "dist/index.js",
    "limit": "200 KB"
  }
]
```

### 性能优化之辅助分析插件

**说明 / 关键点**

- 分析工具用于定位体积热点与慢点：`webpack-bundle-analyzer`、`rollup-plugin-visualizer`、`source-map-explorer`、`esbuild --metafile`、Chrome DevTools Performance、Lighthouse。
- 结合 sourcemaps 能把 bundle 中的代码映射回源代码，找出大体积依赖、重复模块、未 tree-shaken 代码。

**代码 demo（Webpack & Vite 两个常见例）**

Vite（使用 rollup visualizer）—— 已在上题 `vite.config.ts` 演示。

Webpack:

```bash
npm i -D webpack-bundle-analyzer
```

`webpack.config.js`:

```javascript
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  // ...entry/output
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'report.html',
      openAnalyzer: false
    })
  ]
};
```

使用 `source-map-explorer`：

```bash
npm i -D source-map-explorer
# 构建之后：
npx source-map-explorer dist/assets/*.js
```

Chrome DevTools/Lighthouse：在真实设备/模拟慢网下跑 Lighthouse，并结合 Performance Trace 来找 First Contentful Paint / Long Tasks。

### 性能优化之缩小范围(上)

**说明 / 关键点**

- “缩小范围” 的上层策略是从宏观定位到微观：先用 Lighthouse / bundle analyzer 定位是网络 / bundle 体积 / 渲染 / JS 执行问题，再逐步聚焦到具体页面或模块。
- 上层方法：全量采样 -> 选定“慢页面” -> 打开 DevTools（Network/Performance/Memory）抓取 trace。

**步骤**

1. 收集数据：用户热力图、页面访问量、性能埋点（RUM）。
2. 使用 Lighthouse 报表找出症状（如 TTFB / LCP / CLS / FCP）。
3. 根据访问量和影响度确定优化优先级（hot path 优先）。

**代码 demo — 采集与分析示例（简单 RUM）**
`src/rum.ts`

```javascript
// 简单 RUM 上报示例，仅用于采样
export function reportPerf() {
  if (!('performance' in window) || Math.random() > 0.05) return; // 5% 采样
  window.addEventListener('load', () => {
    requestAnimationFrame(() => {
      const timing = performance.timing as any;
      const data = {
        // 注意：Navigation Timing Level 2 推荐使用 PerformanceNavigationTiming
        loadTime: timing.loadEventEnd - timing.navigationStart,
        fcp: performance.getEntriesByType('paint').find(p=>p.name==='first-contentful-paint')?.startTime
      };
      navigator.sendBeacon('/rum', JSON.stringify(data));
    });
  });
}
```

后台只需简单接收并汇总慢页面。

### 性能优化之缩小范围（中）

**说明 / 关键点**

- 中层技术：用 Performance Profiling 的 flame chart 定位 Long Tasks、用 Coverage（Coverage Tab）定位未使用代码/样式、用 Lighthouse 的 “Largest Contentful Paint element” 找到关键渲染资源。
- 热点聚焦后可以用 `--inspect` / `DevTools Profiler` 结合 CPU profile 来找函数占用。

**步骤 & 命令**

1. DevTools → Performance 录制页面加载（选择“Capture DevTools Timeline”），分析 Long Tasks。
2. DevTools → Coverage，Reload with Coverage，查看未使用的 JS/CSS。
3. 若使用 React，开启 Profiler 并在开发环境定位频繁 re-render 的组件。

**示例：在 React 中用 Profiler**

```jsx
import React, { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration) {
  // 将数据上报用于分析
  console.log({ id, phase, actualDuration });
}

export default function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      {/* ... */}
    </Profiler>
  );
}
```

### 性能优化之缩小范围（下）

**说明 / 关键点**

- 下层细化：拿到具体模块后，拆分检查依赖链（import graph）、重复依赖（多版本的 lodash）、第三方大包（charts、i18n、date libs）。
- 常用工具：`npm ls <pkg>`、`depcruise`（dependency-cruiser）来检视依赖图，手动替换或将大库外部化（CDN 或动态 import）。

**示例脚本：检查多版本**

```bash
# 检查 lodash 多版本
npm ls lodash
# 或 pnpm why lodash
pnpm why lodash
```

**替换示例**

- 将 `moment` 替换为 `dayjs`：

```javascript
// before
import moment from 'moment';
console.log(moment().format());

// after
import dayjs from 'dayjs';
console.log(dayjs().format());
```

### 性能优化之忽略模块

**说明 / 关键点**

- 忽略（exclude/ignore）不需要打包的模块：例如测试工具、dev-only 模块、大型 polyfills（按需加载）或 server-only 代码。
- Webpack: `IgnorePlugin`；Rollup / Vite: `external` config；也可以在 `package.json` 的 `browser` 字段或条件导出中排除平台不适用模块。

**代码 demo（webpack IgnorePlugin）**

```javascript
const webpack = require('webpack');
module.exports = {
  plugins: [
    // 忽略 moment 的 locales（常见优化）
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/
    })
  ]
};
```

**Vite / Rollup 外部化**
`vite.config.ts`

```javascript
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['some-node-only-lib']
    }
  }
});
```

------

### 性能优化之缓存与线程（上下）

**说明 / 关键点**

- 缓存：浏览器端（长缓存策略 + content-hash）、Service Worker（Cache API）、HTTP cache headers（Cache-Control, ETag）、构建缓存（esbuild/webpack persistent cache）。
- 线程：将 CPU 密集型任务移到 Web Worker；使用线程池（worker pool）或 node side 的 worker_threads；在构建工具中使用多线程 loader（thread-loader / esbuild 并行）。
- 上下（上）：构建时缓存（faster incremental builds）；（下）：运行时缓存（静态资源 & SW & localStorage）。

**代码 demo — Web Worker 计算 hash（避免主线程卡顿）**
`src/hash.worker.ts`（使用 Vite 支持 worker）

```javascript
// hash.worker.ts
self.onmessage = async (e) => {
  const file = e.data as ArrayBuffer;
  // 在 worker 中计算（示例使用 SubtleCrypto）
  const digest = await crypto.subtle.digest('SHA-256', file);
  const hex = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2,'0')).join('');
  postMessage(hex);
};
```

在主线程中使用：

```javascript
const worker = new Worker(new URL('./hash.worker.ts', import.meta.url));
worker.onmessage = (e) => console.log('hash', e.data);
worker.postMessage(arrayBuffer);
```

**构建缓存（Vite）**
`vite.config.ts`

```javascript
export default defineConfig({
  cacheDir: 'node_modules/.vite_cache',
  build: {
    // 开启 esbuild 的 incremental/incremental 默认行为或使用 vite 内建缓存
  }
});
```

**thread-loader（webpack）**

```javascript
module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          'thread-loader',
          'babel-loader'
        ],
        exclude: /node_modules/
      }
    ]
  }
}
```

### 性能优化之压缩

**说明 / 关键点**

- 代码压缩（JS/CSS）：Terser/Esbuild（esbuild 更快但 terser 在某些场景有更细腻压缩选项）。开启 `drop_console`、`pure_funcs`。
- 传输压缩：gzip / brotli（服务器在构建时预压缩静态资源可提升性能）；HTTP/2+ 更有利于多个小文件传输。
- 压缩图片：用 WebP、AVIF，并在构建或上传管道压缩（sharp、imagemin）。
- 体积对比：构建后产生 `.br` / `.gz` 文件，配合 nginx 配置。

**代码 demo — Vite 插件预压缩 & nginx 配置**
安装预压缩插件：

```bash
npm i -D vite-plugin-compress
vite.config.ts
import compress from 'vite-plugin-compress';
export default defineConfig({
  plugins: [
    compress({ brotli: true, gzip: true })
  ],
  build: { /* ... */ }
});
```

`nginx` 示例（启用 pre-compressed 文件）

```nginx
server {
  location / {
    root /var/www/dist;
    gzip on;
    gzip_static on; # 使用预压缩 .gz
    brotli on;
    brotli_static on; # 如果有预压缩 .br
    add_header Cache-Control "public, max-age=31536000, immutable";
  }
}
```

### 性能优化之样式

**说明 / 关键点**

- CSS 优化包含：Critical CSS（抽取关键首屏样式内联）、按需加载样式、移除未用样式（PurgeCSS / uncss）、CSS 分割（按路由拆分）、避免大范围选择器与深层嵌套（影响渲染性能）。
- 用 CSS-in-JS 或 CSS Modules 可以 scope 样式，减少全局覆盖查找成本；但要注意运行时开销。
- 减少使用 `:root` 过多变量、避免 expensive CSS（box-shadow、大量复杂选择器）、减少 layout-triggering 属性变更（width/height -> transform）。

**代码 demo — Critical CSS 与 Purge（Tailwind 示例）**

Tailwind + purge（tailwind.config）

```javascript
module.exports = {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  // ...
}
```

抽取 Critical CSS（构建后用 `critters` 或 `critical`）

```bash
npm i -D critters-webpack-plugin
```

`webpack.config.js`:

```javascript
const Critters = require('critters-webpack-plugin');
module.exports = {
  plugins: [
    new Critters({
      // inline: true,
      preload: 'swap'
    })
  ]
}
```

按路由加载样式（React + dynamic import）

```javascript
const LazyPage = React.lazy(() => import('./page-with-big-css'));
```

该页面的 CSS 会随 chunk 被加载。

------

### 性能优化之环境变量（上）

**说明 / 关键点**

- 环境变量影响构建行为与打包结果（如是否启用 mock、是否开启 source map、API 域名等）。
- 在构建时通过 define 插件（Vite 的 `define` / webpack 的 DefinePlugin）注入常量，允许构建器做 Dead Code Elimination（DCE）。
- 使用 `process.env.NODE_ENV === 'production'` 作为分支可被 tree-shaking 删除不需要的代码块。

**示例（Vite define）**
`vite.config.ts`

```javascript
export default defineConfig(({ mode }) => ({
  define: {
    __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
    __APP_ENV__: JSON.stringify(process.env.APP_ENV || mode)
  }
}));
```

**在代码中使用**

```javascript
if (process.env.NODE_ENV !== 'production') {
  console.log('debug only');
}
// 或者使用 define 常量
if (__APP_ENV__ === 'development') {
  // dev only
}
```

这样生产构建会把分支去掉，减少体积。

### 性能优化之环境变量（下）

**细节与实战：条件导入 / 条件打包**

- 利用条件编译（Define + dead code elimination）避免引入大型 dev-only lib（例如：只在 dev 引入 `why-did-you-render`）。
- 使用 `import()` 在运行时按需加载环境相关模块。
- 对第三方库采用 externals/CDN 在生产构建时替换。

**代码 demo — dev-only 包**

```javascript
if (process.env.NODE_ENV === 'development') {
  // 这里使用 dynamic import，生产构建下该分支被去掉
  import('why-did-you-render').then(wdyr => {
    // init
  });
}
```

在 webpack/Vite 中开启 Terser/Esbuild 的 DCE 会消除掉这些分支。

### 性能优化之treeshaking

**说明 / 关键点**

- Treeshaking 的前提：使用 ES module（`import`/`export`）与支持 treeshake 的打包器（Rollup/Vite/webpack 在 production 模式）。
- package.json 的 `sideEffects` 字段需正确标识模块是否有副作用（false 或列出有副作用的文件）。
- 避免使用会阻止 tree-shaking 的语法（例如：`require()`、动态构建的大量 `export *`、对整个库做 `import * as` 然后引用属性会阻碍某些优化）。
- 优化第三方库：优先选择提供 ESM 的包（`lodash-es` 而非 `lodash`）。

**代码 demo**
`package.json`

```json
{
  "sideEffects": [
    "*.css",
    "src/some-global-polyfill.js"
  ]
}
```

使用 `lodash-es`：

```javascript
import { debounce } from 'lodash-es';
```

避免 `import * as _ from 'lodash'`，会导致无法去掉没用的函数。

### 性能优化之代码分割（上）

**说明 / 关键点**

- 代码分割分为路由级分割、组件级分割、库分割（vendor）三类。
- 动态 import（`import()` / React.lazy）是最常用方式；手动设置 `manualChunks` 进行长期缓存优化。
- 把首次渲染所需的最少代码放在第一页，其它通过懒加载按需拉取。

**示例（React + Vite）**

```javascript
// route-based lazy
import React, { Suspense } from 'react';
const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>loading</div>}>
  {/* 根据路由加载 */}
  </Suspense>
  );
}
```

`vite.config.ts` 手工分割：

```javascript
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react')) return 'vendor_react';
            return 'vendor';
          }
        }
      }
    }
  }
});
```

### 性能优化之代码分割（下）

**更深入：预加载、预取、优先级控制**

- 使用 `<link rel="prefetch">` 或 `<link rel="preload">` 控制资源优先级。
- React Router 等可以在空闲时间提前加载下一个页面的 chunk（link prefetch 或 `requestIdleCallback`）。
- 注意：过度预加载会浪费带宽，应基于访问概率做智能预取。

**代码 demo — 在路由改变前预fetch**

```javascript
// 简单的 prefetch util
export function prefetchComponent(loader: () => Promise<any>) {
  if ('requestIdleCallback' in window) {
    (window as any).requestIdleCallback(() => {
      loader();
    });
  } else {
    setTimeout(() => loader(), 2000);
  }
}

// 用法：在 hover 或导航预测时调用
button.addEventListener('mouseover', () => prefetchComponent(() => import('./Dashboard')));
```

### 性能优化之提取公共组件（上）

**说明 / 关键点**

- 公共组件提取分为代码级（common chunk / shared library）和组件库（monorepo 的 shared 包）。
- 好处：减少重复代码、便于缓存和复用。缺点：过度共享会增加首屏体积（如果把不常用组件放公共 chunk）。
- 方式：手动 `manualChunks`、使用 monorepo + pnpm workspace 抽出 `@company/ui` 包、或在运行时 CDN 加载公共 UI 库。

**代码 demo — monorepo shared package（pnpm workspace）**
仓库结构：

```javascript
/packages
  /ui (shared components)
  /app (main app depends on @company/ui)
packages/ui/package.json
{
  "name": "@company/ui",
  "version": "1.0.0",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "sideEffects": false
}
```

组件示例：

```tsx
// packages/ui/src/Button.tsx
import React from 'react';
export const Button = (props) => <button {...props} />;
```

主应用直接 `import { Button } from '@company/ui'`，构建工具会把它作为单独包处理，利于复用与缓存。

### 性能优化之提取公共组件（中）

**策略 & 实践**

- 按需打包共享组件（shared chunk），把稳定且常用的组件放到 vendor/chunk，减少多次重复加载。
- 做「按需导出」：不要把整个 component 库以一个巨型 bundle 导出，支持按文件导入（`import { Button } from '@company/ui'` 或 `import Button from '@company/ui/Button'`）。
- 在构建时利用 `splitChunks`（webpack）或 `manualChunks`（rollup/vite）把常用组件单独分离。

**webpack splitChunks 示例**

```javascript
optimization: {
  splitChunks: {
    chunks: 'all',
      cacheGroups: {
      commons: {
        name: 'commons',
          test: /[\\/]src[\\/]components[\\/]/,
          minChunks: 2,
          priority: 10
      }
    }
  }
}
```

### 性能优化之提取公共组件（下）

**进阶：运行时共享（Module Federation / microfrontends）**

- 对于大型多团队项目，可使用 Module Federation（webpack 5）或微前端框架（single-spa/Wujie）实现运行时共享组件、按需加载并互相复用。
- 好处：团队独立部署、减小单体包；坏处：复杂度高、版本兼容挑战。

**Module Federation 简单示例（host/remote）**
remote `webpack.config.js`:

```javascript
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button'
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
    })
  ]
};
```

host:

```javascript
new ModuleFederationPlugin({
  remotes: {
    remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js'
  },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
});
```

Host 可直接动态加载 remote 的 Button 组件。

##  各大互联网大厂小程序相关面试真题&答案

### 微信小程序的实现原理

**要点（概念层面）**

- 小程序是一个运行在微信容器（WeChat runtime）内的轻量级应用。它由两部分组成：

- - **视图层（View）**：WXML + WXSS（类似 HTML/CSS）在微信内置渲染引擎里渲染，是受限的 DOM。
  - **逻辑层（Logic）**：JS 运行在 V8/C++ 等环境里，与视图层通过**消息机制（异步桥）**交互（通过 setData / 事件回传等）。

- **组件化 + 沙箱**：每个小程序被沙箱化运行，权限受限制（API 由微信提供），资源使用受限（包体、API 调用等）。
- **网络/存储**：网络请求走 wx.request（有域名白名单），本地有缓存（wx.setStorageSync）、文件系统（wx.getFileSystemManager）。
- **分包/按需加载**：支持分包加载（主包 + 分包 + 子包），可优化首包体积。
- **生命周期与页面栈**：小程序维护页面栈（最多10层），路由 API 在容器层实现（navigateTo、redirectTo 等）。

**简短图示（文字）**
用户操作 → 微信容器 → 视图层渲染 + 逻辑层 JS 处理 → 通过异步消息桥修改视图（setData）或调用原生 API（支付、定位等）

**代码演示（最小运行示例）**
项目结构：

```bash
/minapp-demo
  /pages/index/index.wxml
  /pages/index/index.js
  /app.json
  /app.js
  /app.wxss
app.json
{
  "pages": ["pages/index/index"],
  "window": {
    "navigationBarTitleText": "示例"
  }
}
app.js
App({
  onLaunch() {
    console.log('App onLaunch');
    // 初始化逻辑，例如获取用户授权、读取本地缓存等
  }
});
pages/index/index.wxml
<view class="container">
  <text>{{msg}}</text>
  <button bindtap="onTap">点击改变数据</button>
</view>
pages/index/index.js
Page({
  data: { msg: 'Hello 小程序' },
  onTap() {
    // 逻辑层修改数据后，视图层刷新
    this.setData({ msg: '数据已更新：' + Date.now() });
  }
});
app.wxss
.container { padding: 20px; }
```

### 微信小程序的支付流程

**要点（高层流程）**

1. 小程序调用 `wx.login` 获取 `code`（临时登录凭证），或直接走支付流程时先登录拿到 user info（非必需）。
2. 小程序向自己后端发起“创建支付订单”请求（包含订单信息、用户标识等）。
3. 后端使用微信商户平台的 API（统一下单 API：`/pay/unifiedorder`）向微信服务器申请生成 `prepay_id`。后端需要签名（商户 key）、商户号、appId、notify_url 等。
4. 微信返回 `prepay_id` 给后端，后端将所需参数（nonceStr、timeStamp、package=prepay_id、signType、paySign）签名后返回给小程序。
5. 小程序调用 `wx.requestPayment({...})` 使用后端返回的参数拉起支付界面。
6. 支付完成后，微信服务器异步回调商户后端的 `notify_url`（需校验签名并处理订单状态）。
7. 后端更新订单状态并通知小程序/前端（可通过轮询/推送/消息）。

**关键点与注意事项**

- 所有与商户密钥相关的签名必须在后端完成（不能在客户端做），商户 key 不能暴露。
- `notify_url` 必须可公网访问且可校验签名。
- 小程序 prepay 流程需要在微信商户平台配置小程序 appId、商户号、API key、证书（部分接口）。
- 调试时使用沙箱或真机环境，微信开发者工具支持模拟 `wx.requestPayment` 但真实支付需真机。

**示例代码（后端 Node + 小程序端）**

后端：`server.js`（Express + axios + xml2js）

这是最简示例，真实项目要做签名、错误处理、幂等、证书、回调验证等更严谨的实现。

```javascript
// server.js
const express = require('express');
const axios = require('axios');
const xml2js = require('xml2js');
const crypto = require('crypto');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.text({ type: '*/xml' }));
app.use(express.json());

const MCH_ID = 'your_mch_id';
const APP_ID = 'your_appid';
const API_KEY = 'your_api_key';
const UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder';

// util: build sign (MD5 uppercase)
function buildSign(params) {
  const ordered = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
  return crypto.createHash('md5').update(ordered + `&key=${API_KEY}`).digest('hex').toUpperCase();
}

function toXml(obj) {
  const builder = new xml2js.Builder({ headless: true, rootName: 'xml' });
  return builder.buildObject(obj);
}

function parseXml(xml) {
  return xml2js.parseStringPromise(xml, { explicitArray: false, trim: true });
}

// 创建预支付订单
app.post('/create-unified-order', async (req, res) => {
  const { openid, out_trade_no, total_fee, body } = req.body;
  const nonce_str = crypto.randomBytes(16).toString('hex');
  const params = {
    appid: APP_ID,
    mch_id: MCH_ID,
    nonce_str,
    body,
    out_trade_no,
    total_fee: String(total_fee), // 分
    spbill_create_ip: req.ip.replace('::ffff:', ''),
    notify_url: 'https://yourdomain.com/pay/notify',
    trade_type: 'JSAPI',
    openid
  };
  params.sign = buildSign(params);
  const xml = toXml(params);

  try {
    const r = await axios.post(UNIFIED_ORDER_URL, xml, { headers: { 'Content-Type': 'text/xml' } });
    const parsed = await parseXml(r.data);
    const result = parsed.xml;
    if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
      // 返回 prepay_id 给小程序（后端需再生成签名给前端）
      const prepay_id = result.prepay_id;
      const timeStamp = String(Math.floor(Date.now() / 1000));
      const paySignParams = {
        appId: APP_ID,
        timeStamp,
        nonceStr: nonce_str,
        package: `prepay_id=${prepay_id}`,
        signType: 'MD5'
      };
      const paySign = buildSign(paySignParams);
      res.json({ success: true, data: { ...paySignParams, paySign } });
    } else {
      res.json({ success: false, error: result });
    }
  } catch (err) {
    console.error(err);
    res.status(500).json({ success: false, error: err.message });
  }
});

// 支付结果通知（微信回调）
app.post('/pay/notify', async (req, res) => {
  // req.body is xml
  try {
    const parsed = await parseXml(req.body);
    const data = parsed.xml;
    // TODO: 校验签名，更新订单状态
    // 返回给微信 SUCCESS
    const ok = { return_code: 'SUCCESS', return_msg: 'OK' };
    res.set('Content-Type', 'text/xml').send(toXml(ok));
  } catch (e) {
    res.status(500).send('error');
  }
});

app.listen(3000, () => console.log('server listening 3000'));
```

小程序端：
`pages/pay/pay.js`

```javascript
// 调用后端创建订单并拉起支付
Page({
  async createOrderAndPay() {
    try {
      const openid = wx.getStorageSync('openid'); // 事先登录并保存
      const resp = await wx.request({ 
        url: 'https://yourdomain.com/create-unified-order',
        method: 'POST',
        data: {
          openid,
          body: '商品描述',
          out_trade_no: 'order' + Date.now(),
          total_fee: 1 // 分
        }
      });
      const data = resp.data.data;
      wx.requestPayment({
        timeStamp: data.timeStamp,
        nonceStr: data.nonceStr,
        package: data.package,
        signType: data.signType,
        paySign: data.paySign,
        success(res) { console.log('支付成功', res); },
        fail(err) { console.error('支付失败', err); }
      });
    } catch (err) {
      console.error(err);
    }
  }
});
```

### 提高微信小程序应用速度的手段（性能优化）

按“启动/渲染/网络/运行时”分类列出常用手段，并给代码/配置示例。

**启动/包体与分包**

- **主包最小化**：把非首屏资源放到分包、子包、异步组件里。

- - `app.json` 中配置 `subPackages`。

```javascript
{
  "subPackages": [
    {
      "root": "packageA",
      "pages": ["pages/a/index"]
    }
  ]
}
```

- **使用插件 & 云函数按需加载**：将耗时逻辑放到后端/云函数。

**渲染优化**

- **尽量减少 setData 的数据量与频繁调用**：只更新必要字段，批量更新。

```javascript
// 不建议：每项单独 setData
this.setData({ a: 1 });
this.setData({ b: 2 });
// 建议：
this.setData({ a: 1, b: 2 });
```

- **组件化/纯数据组件**：避免把大量无关数据放在父组件导致不必要重渲染。
- **WXS 用于小量计算/模板逻辑**：在视图层做少量处理，减少逻辑层通信。

```javascript
// utils.wxs
module.exports = {
  format: function(s) { return s + '!' }
}
```

**网络/请求**

- **开启 gzip，使用 CDN**：后端返回静态资源使用 CDN；接口响应体支持 gzip。
- **缓存策略**：使用 `wx.setStorageSync` 缓存列表/静态数据，利用 ETag/If-Modified-Since。
- **合并请求与接口分页**：接口聚合或多路合并减少网络多次往返。

**列表渲染**

- **虚拟列表**（长列表不可一次渲染）：用第三方或手写虚拟滚动（只渲染可视区）。
- **分段渲染**：首屏先渲染必要项，非关键延后。

**主线程/计算密集**

- **使用 Worker（小程序支持 Worker）**：把 hash、图片压缩等放到 Worker，避免卡 UI。

```javascript
// main.js
const worker = wx.createWorker('workers/task.js');
worker.postMessage({ cmd: 'hash', filePath: '/tmp/x' });
worker.onMessage(res => console.log(res));
workers/task.js
// worker script
onMessage((msg) => {
  if (msg.cmd === 'hash') {
    // 假装计算
    postMessage({ result: 'done' });
  }
});
```

**渲染性能工具与监控**

- 使用小程序性能面板（微信开发者工具）查看 FPS、页面渲染耗时；在产品中埋点采集首屏时间、接口耗时等。

**代码示例：分包 + 延迟加载组件**
`app.json`

```javascript
{
  "pages": ["pages/index/index"],
    "subPackages": [
    { "root": "packageLarge", "pages": ["pages/large/index"] }
  ]
}
```

在首页点击跳转时再加载：

```javascript
wx.navigateTo({ url: '/packageLarge/pages/large/index' });
```

### 微信小程序中路由跳转方式与区别

微信提供 5 种主要路由 API：

1. `wx.navigateTo({url})`

- - 向页面栈添加一页（保留当前页面），最多 10 层。用于非 tab 页面之间的跳转。
  - 返回后页面保持状态（未卸载）。

1. `wx.redirectTo({url})`

- - 替换当前页面（不保留当前页面），不会增加页面栈深度。用于不需要返回的场景（例如登录后跳转）。

1. `wx.switchTab({url})`

- - 切换到 tabBar 页面（tab 必须在 app.json 的 tabBar 中配置）。会关闭所有非 tab 页面。用于底部导航切换。

1. `wx.reLaunch({url})`

- - 关闭所有页面并打开到指定页面。通常用于重大流程后重置栈（如登出后回到登录页）。

1. `wx.navigateBack({delta})`

- - 回退到上一个页面或指定层级。不能用于打开新页面。

**区别总结**

- 保留与否：`navigateTo` 保留当前，`redirectTo` 不保留，`reLaunch` 全部清空。
- 目标类型：`switchTab` 只能跳 tabBar 页面。
- 场景：`navigateTo` 用于细节页，`redirectTo` 用于替换页面（如一步完成跳转），`reLaunch` 用于重置整个 app 流程。

**示例代码**

```javascript
// 从首页 navigateTo 详情页
wx.navigateTo({ url: '/pages/detail/index?id=123' });

// 登录后替换到主页（不需要后退）
wx.redirectTo({ url: '/pages/home/index' });

// 切换底部 tab
wx.switchTab({ url: '/pages/tab/home' });

// 完成结账后重启到成功页
wx.reLaunch({ url: '/pages/success/index' });

// 返回两级页面
wx.navigateBack({ delta: 2 });
```

### 微信小程序的登录流程

**要点（标准流程）**

1. **小程序端**：调用 `wx.login()` 获得 `code`（有效期短）。
2. **小程序端**：把 `code` 发给**自己后端**（不要直接把 code 发给微信）。
3. **后端** 调用微信服务器的 `code2session` 接口（`https://api.weixin.qq.com/sns/jscode2session`），传 `appId`、`secret`、`js_code`，获取 `openid`、`session_key`（以及 `unionid` 可选）。
4. **后端** 使用 `session_key` 进行后续数据解密（如解密 `getUserInfo` 的 encryptedData），或者基于 `openid` 创造自家用户体系并发放自签名 Token（例如 JWT）。
5. **小程序端** 存储后端返回的 token（wx.setStorageSync），后续接口携带 token 做鉴权。

**注意点**

- `code` 要在后端换取 `session_key`，并且不要把 `session_key` 或 `secret` 踢到客户端。
- 如果需要解密用户信息（encryptedData + iv），必须在后端用 `session_key` 解密以确保安全。
- `session_key` 有效期，后端需要设计登录态续期（刷新或重新 login）。

**示例代码（小程序 + 后端 Node）**

小程序端：

```javascript
// login.js
async function loginToServer() {
  const res = await new Promise((resolve, reject) => {
    wx.login({ success: resolve, fail: reject });
  });
  const code = res.code;
  // 调用自己服务端
  const r = await wx.request({ url: 'https://yourdomain.com/auth/login', method: 'POST', data: { code } });
  // 后端返回 token
  wx.setStorageSync('token', r.data.token);
}
```

后端（Node + axios）：

```javascript
// auth.js
const express = require('express');
const axios = require('axios');
const jwt = require('jsonwebtoken');
const APPID = 'your_appid';
const SECRET = 'your_secret';

const router = express.Router();

router.post('/login', async (req, res) => {
  const { code } = req.body;
  const r = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
    params: { appid: APPID, secret: SECRET, js_code: code, grant_type: 'authorization_code' }
  });
  const data = r.data;
  if (data.errcode) return res.status(500).json({ error: data });
  const openid = data.openid;
  // 查库/创建用户
  const user = { id: openid };
  const token = jwt.sign({ uid: user.id }, 'your_jwt_key', { expiresIn: '7d' });
  res.json({ token });
});

module.exports = router;
```

### 微信小程序的生命周期函数有哪些

分两类：**App 生命周期** 与 **Page（页面）生命周期**。

**App 生命周期（在** `**app.js**`**）**

- `onLaunch(options)`：小程序初始化时触发，且只触发一次。可做全局初始化、获取系统信息、登录等。
- `onShow(options)`：小程序启动，或从后台进入前台时触发。`options` 包含 `scene`、`query` 等。
- `onHide()`：小程序从前台进入后台时触发。
- `onError(msg)`：脚本错误/API 调用报错时触发。
- `onPageNotFound(res)`：页面不存在时触发（可重定向）。

**Page 生命周期（在** `**pages/.../index.js**`**）**

- `onLoad(options)`：页面加载时触发（只触发一次），可接收路由参数 `options`。
- `onShow()`：页面显示/切到前台时触发（每次显示都会触发）。
- `onReady()`：页面第一次渲染完成后触发（只触发一次）。
- `onHide()`：页面被遮挡或切到后台时触发。
- `onUnload()`：页面被关闭时触发（navigateBack 或 redirectTo 替换会触发）。
- 页面相关事件：

- - `onPullDownRefresh()`：下拉刷新触发（需在 page config 打开）。
  - `onReachBottom()`：滚动到底部触发。
  - `onShareAppMessage()`：用户点击右上角转发时触发（返回转发信息）。
  - `onResize()`：窗口尺寸变化时触发（部分平台）。

- 组件也有生命周期（如 `lifetimes` / `attached` / `detached` 等）。

**示例**

```javascript
Page({
  onLoad(options) { console.log('onLoad', options); },
  onShow() { console.log('onShow'); },
  onReady() { console.log('onReady'); },
  onHide() { console.log('onHide'); },
  onUnload() { console.log('onUnload'); },
  onPullDownRefresh() { console.log('refresh'); wx.stopPullDownRefresh(); },
  onReachBottom() { console.log('reach bottom'); }
});
```

### 你对微信小程序的理解？优缺点？

**理解（一句话）**：
微信小程序是“在微信生态内可快速触达、低门槛发布、受限容器环境下运行”的轻量级应用形态，适合轻量业务、入口导流、工具型场景。

**优点**

- **低门槛分发**：用户无需安装即可使用，强大的流量入口（公众号、社群、扫一扫、分享等）。
- **统一平台 SDK**：微信提供支付、授权、位置、开放能力、模板消息等一站式能力。
- **快速迭代**：发布流程简单，上线速度快。
- **跨平台（近）**：在微信生态内能覆盖 iOS/Android，同时对开发者门槛友好。

**缺点 / 局限**

- **受限的能力**：沙箱限制、API 受限（如复杂 native 能力弱），不能随意访问设备底层。
- **包体与性能限制**：首包体积限制、页面栈限制（10 层），渲染能力不及原生应用，在复杂可视化、高性能场景下受限。
- **平台依赖**：依赖微信生态，商业规则与审核可能限制某些功能。
- **调试与兼容性**：微信版本差异、真机差异、不同手机厂商表现差异等。

**面试展示角度（建议回答模板）**

- 说明应用场景（适合电商轻量成交页、工具、活动页、SaaS 轻量客户端等），并给出优缺点，最后给出一个 mitigation 策略，例如在高性能场景用原生小程序或 H5 + 原生容器，或把复杂计算放服务端/Worker。

------

### 代码合集（附更多实用示例）

### A. setData 性能优化示例（批量更新/最小化更新）

```javascript
// 误用：每次都 setData
for (let i = 0; i < 100; i++) {
  this.setData({ ['items[' + i + '].value']: i });
}

// 优化：先构建对象，再一次 setData
const update = {};
for (let i = 0; i < 100; i++) update['items[' + i + '].value'] = i;
this.setData(update);
```

### B. 分包示例（app.json）

```json
{
  "pages":["pages/index/index"],
  "subPackages":[
    {
      "root":"packageA",
      "pages":["pages/a/index","pages/a/detail"]
    }
  ],
  "tabBar": {
    "list": [{ "pagePath": "pages/index/index", "text": "首页" }]
  }
}
```

------

### 面试答题小策略（如何包装回答）

- 给出**定义 → 原理 → 场景 → 注意点 → 代码示例** 五段式回答，既有理论又能展示工程实践。
- 遇到开放题（性能/优缺点），用“分层/分类”方式列举（启动/渲染/网络/运算），并至少给 2 个真实项目中可落地的优化措施。
- 代码 demo 要能直接运行（可脱敏），并标注“关键安全点（签名/密钥在后端）”。

## 各大互联网大厂场景题面试真题&答案

### 01 场景题 — 小程序双线程模型（微信小程序 / 类似平台）

**目标**：解释小程序“双线程/双进程”模型（主线程/UI 线程 + 后台逻辑线程 / Worker）与实现注意点。

#### 说明（面试要点）

- 小程序通常存在 `主线程`（渲染 & UI 调度）和 `逻辑线程`（JS 引擎运行）——不同平台细节不同（微信、支付宝等实现差异）。
- 小程序支持 `Web Worker`/子线程（后端运行 heavy 任务，如 hash、压缩、计算），避免阻塞 UI/渲染。
- 跨线程通讯通过 `postMessage`（序列化）进行，注意不能直接共享 DOM、复杂引用。
- 可利用 `Worker` 做：图片压缩、文件切片、hash 计算、长耗时计算。
- 注意：资源限制（Worker 数量限制、内存限制），序列化成本（大量数据可用 Transferable objects）。

#### 示例（微信小程序 Worker）

```js
// main.js
if (wx.canIUse('createWorker')) {
  const worker = wx.createWorker('workers/task.js');
  worker.postMessage({ cmd: 'hash', filePath: '/path/to/file' });
  worker.onMessage(function(res) {
    console.log('worker result', res);
  });
}
// workers/task.js
onMessage(function(msg) {
  if (msg.cmd === 'hash') {
    // do heavy work
    const result = doHash(msg.filePath);
    postMessage({ result });
  }
});
```



### 02 场景面试 — 大文件上传（分片、断点续传、失败重试、hash 校验、Web Worker 切片）

**目标**：实现稳定的大文件上传方案，支持断点续传、分片重试、并发上传、校验（MD5/SHA1）、避免主线程卡顿（Worker 做 hash 切片）。

#### 关键点

- 切片（chunkSize，例如 2MB）
- 每片带索引、hash、size
- 后端提供 `/upload/init` (创建任务)、`/upload/chunk` (单片上传)、`/upload/complete` (合并)
- 客户端保存上传进度（localStorage/IndexedDB）
- Worker 计算文件 hash，且负责切片准备
- 并发上传（PromisePool）
- 单片失败重试（指数退避、最大重试次数）
- 断点续传：init 返回已经接收的 chunk 列表

#### 前端（核心）代码（包含 Worker）

// worker.js（Web Worker: 负责计算 hash 与切片元数据）

```javascript
// worker.js
self.onmessage = async (e) => {
  const { file, chunkSize } = e.data;
  const total = Math.ceil(file.size / chunkSize);
  const chunks = [];
  for (let i = 0; i < total; i++) {
    const start = i * chunkSize;
    const end = Math.min(file.size, start + chunkSize);
    const blob = file.slice(start, end);
    // 可以额外计算分片的简单 hash（如md5），这里示例不引入库，使用 size+index
    chunks.push({ index: i, start, end, size: blob.size });
    // 为避免一次加载太多，可 postProgress
    if (i % 50 === 0) self.postMessage({ progress: i / total });
  }
  // 计算文件简单 fingerprint（注意：真实md5需引入spark-md5等）
  const fingerprint = `${file.name}-${file.size}-${file.lastModified}`;
  self.postMessage({ done: true, chunks, fingerprint });
};
```

// upload.tsx（前端主逻辑）

```tsx
// upload.ts (React + TS, 关键函数）
type InitResp = { uploadId: string; uploaded?: number[] };

async function initUpload(apiBase: string, filename: string, totalChunks: number, fingerprint: string): Promise<InitResp> {
  const res = await fetch(`${apiBase}/upload/init`, {
    method: "POST",
    headers: {"Content-Type":"application/json"},
    body: JSON.stringify({ filename, totalChunks, fingerprint })
  });
  return res.json();
}

async function uploadChunk(apiBase: string, uploadId: string, chunkIndex: number, chunkBlob: Blob, retry=3) {
  const form = new FormData();
  form.append("uploadId", uploadId);
  form.append("index", String(chunkIndex));
  form.append("file", chunkBlob);
  for (let attempt=0; attempt<retry; attempt++) {
    try {
      const resp = await fetch(`${apiBase}/upload/chunk`, { method:"POST", body: form });
      if (resp.ok) return await resp.json();
    } catch (err) {
      const wait = Math.pow(2, attempt) * 300;
      await new Promise(r => setTimeout(r, wait));
    }
  }
  throw new Error(`Chunk ${chunkIndex} upload failed`);
}

export async function uploadFileWithWorker(file: File, apiBase: string) {
  const chunkSize = 2 * 1024 * 1024; // 2MB
  const worker = new Worker("/worker.js");
  const chunksMeta: any[] = await new Promise((resolve) => {
    worker.onmessage = (e) => {
      if (e.data.done) {
        resolve(e.data.chunks.map((c)=>({...c})));
      }
    };
    worker.postMessage({ file, chunkSize });
  });

  const fingerprint = `${file.name}-${file.size}-${file.lastModified}`;
  const totalChunks = chunksMeta.length;
  const init = await initUpload(apiBase, file.name, totalChunks, fingerprint);
  const uploadId = init.uploadId;
  const uploadedSet = new Set(init.uploaded || []);

  // 并发池
  const concurrency = 4;
  const queue = chunksMeta.map((meta, idx) => ({ ...meta, index: idx }));
  let active = 0;
  let i = 0;

  return new Promise<void>((resolve, reject) => {
    const runNext = async () => {
      if (i >= queue.length && active === 0) {
        // 请求合并
        await fetch(`${apiBase}/upload/complete`, { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({ uploadId }) });
        resolve();
        return;
      }
      while (active < concurrency && i < queue.length) {
        const chunk = queue[i++];
        if (uploadedSet.has(chunk.index)) continue;
        active++;
        (async () => {
          const blob = file.slice(chunk.start, chunk.end);
          try {
            await uploadChunk(apiBase, uploadId, chunk.index, blob);
            // 可更新本地进度
          } catch (err) {
            reject(err);
            return;
          } finally {
            active--;
            runNext();
          }
        })();
      }
    };
    runNext();
  });
}
```

#### 后端（伪代码 Node/Express）

```javascript
// /upload/init
// 接收 fingerprint, totalChunks, filename
// 如果已存在 unfinished record, 返回已接收的 chunk 索引列表
// 否则创建 uploadId = uuid(), 存 db (uploadId, fingerprint, totalChunks, uploaded:[])
// /upload/chunk
// 接收 uploadId, index, file
// 将 chunk 存储到临时目录 /uploads/{uploadId}/{index}
// 标记 DB: push index
// /upload/complete
// 合并文件（按 index 读取文件流 concat），保存为目标文件；验证大小/hash；清理临时
```

#### 面试亮点要点（说给面试官听）

- 说明为什么用 Worker（避免主线程卡顿：hash、切片），为什么要并发限制（避免服务器/网络拥塞），为什么要断点续传（网络不稳定），如何保证幂等（uploadId+index）、安全（token、签名）、如何处理碎片合并的内存优化（流式合并避免把所有片读到内存）。

### 03 场景面试 — 页面截图（前端实现网页截屏/保存/导出：整页、可视区、带滚动）

**目标**：实现页面截图（可视区 / 整页），并支持导出 PNG/PDF。
**关键点**：Canvas 渲染（html2canvas）、整页滚动截图（分段拼接）、处理跨域图片（CORS 或 proxy）、高 DPI（devicePixelRatio），性能与内存限制。

#### 方案

- 使用成熟库 html2canvas 或 puppeteer（后端无头浏览器）；
- 前端：html2canvas 支持大部分用例；整页利用 `window.scrollTo` + 分片截图拼接到 canvas；
- 特殊：如果页面中有跨域图片，需要确保图片有 CORS header 或通过后端代理。

#### 前端示例（使用 html2canvas，整页分片）

```javascript
import html2canvas from "html2canvas";

async function captureVisible(element: HTMLElement) {
  const canvas = await html2canvas(element, { useCORS: true, scale: window.devicePixelRatio || 1 });
  return canvas.toDataURL("image/png");
}

// 整页截图（分段拼接）
async function captureFullPage() {
  const body = document.body;
  const totalHeight = Math.max(body.scrollHeight, document.documentElement.scrollHeight);
  const viewportHeight = window.innerHeight;
  const canvases = [];
  for (let y = 0; y < totalHeight; y += viewportHeight) {
    window.scrollTo(0, y);
    // 等待重排
    await new Promise(r => setTimeout(r, 200));
    const base = await html2canvas(document.documentElement, { useCORS: true, scale: 1 });
    canvases.push(base);
  }
  // 拼接
  const finalCanvas = document.createElement("canvas");
  finalCanvas.width = canvases[0].width;
  finalCanvas.height = totalHeight;
  const ctx = finalCanvas.getContext("2d")!;
  let offset = 0;
  canvases.forEach(c => {
    ctx.drawImage(c, 0, offset);
    offset += c.height;
  });
  return finalCanvas.toDataURL("image/png");
}
```

#### 后端无头浏览器（面试高分项）

- 用 Puppeteer 做后端渲染截图，更稳定，适合生成 PDF、定时抓取。

### 场景面试 — 移动端适配

**目标**：从 UI / 性能 / 体验角度实现兼容各种屏幕（安卓/iOS、机型差异）。
**关键点**：视口 meta、弹性布局、响应式单位（rem / vw）、媒体查询、图片/资源按需加载、触控优化（点击延迟、滚动节流）、高 DPI 资源、字体和行高、可访问性。

#### 推荐策略（实战）

1. 视口 `<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">`
2. 使用 REM：根字体动态根据设备宽度设置（如 375px 对应 1rem = 16px）
3. CSS Grid / Flexbox + 断点设计（mobile-first）
4. 图片：`srcset` + `picture` 或按 DPR 提供 @1x,@2x
5. 性能：懒加载（IntersectionObserver）、长列表虚拟化
6. 触控：`touch-action` 与避免 `300ms` 点击问题（现代浏览器已解决）
7. 软键盘：输入框聚焦时避免布局跳动（监听 `visualViewport`）

#### 示例：动态 root font-size（适配）

```javascript
/* index.css */
html { font-size: 16px; } /* fallback */
// adapt.js
function setRem(baseWidth = 375, baseRem = 16) {
  const dpr = Math.min(window.devicePixelRatio || 1, 3);
  const clientWidth = document.documentElement.clientWidth || window.innerWidth;
  const rem = (clientWidth / baseWidth) * baseRem;
  document.documentElement.style.fontSize = rem + "px";
  document.documentElement.setAttribute("data-dpr", String(dpr));
}
window.addEventListener("resize", () => setRem());
setRem();
```

### 场景题 — 如何修改第三方 npm 包（例如：修改 bug / 定制化需求）

**目标**：在不直接修改 node_modules 的前提下安全地修改第三方包并可维护地使用。
**方法**：

1. Fork + Patch + Publish to private registry（公司内 npm 私服 / GitHub Packages）；
2. 使用 `patch-package`：在项目中对 node_modules 修改后，运行 `npx patch-package pkg-name`，并把 patch 提交到仓库，安装后 `postinstall` 自动应用；
3. 提交 PR 给原作者（长期方案）；
4. 使用 alias/替换（pnpm / webpack alias / vite resolve.alias）重定向到本地修改版本。

#### 示例：使用 patch-package（推荐面试答案）

```shell
# 安装 patch-package
npm install patch-package --save-dev

# 修改 node_modules/some-lib/index.js
# 生成补丁
npx patch-package some-lib

# package.json
{
  "scripts": {
    "postinstall": "patch-package"
  }
}
```

#### 复杂方案（Fork & private publish）

- Fork 到自己 org，修改代码，维护版本号，发布到私服 `npm publish --registry http://your-registry`，package.json 指向该私服版本。

### 场景题 — 一个链接两应用（同一链接/按钮在两个不同应用中打开/路由）

**场景解释**：单一 URL 同时被多个子应用/入口使用（微前端场景），或一个链接在不同应用中打开（主应用/子应用共享跳转）。

#### 解决思路

- 统一路由策略（例如：hash / path）并通过 query 参数或 fragment 指明目标 app。
- 主入口处理：当链接进入主应用，根据 path 或参数决定 mount 哪个子应用（微前端框架如 qiankun/Wujie/micro-app）。
- 如果需要在两个应用中同时打开（一个新窗口打开另一个），使用 target + window.open with URL + params。

#### 示例（微前端 qiankun 路由策略）

```javascript
// link: https://host/app?app=react-demo#/pageA
// 主应用读取 app 参数，决定 mount 子应用并将路由传递
const url = new URL(location.href);
const targetApp = url.searchParams.get("app"); // react-demo 或 vue-demo
// 主应用加载对应子应用并传入生命周期 props（例如 initialPath）
registerMicroApps([
  {
    name: 'react-demo',
    entry: '//localhost:7100',
    container: '#subapp',
    activeRule: (location) => location.search.includes('app=react-demo')
  },
  // ...
]);
```

### 场景题 — 实现网页加载进度条

**目标**：页面加载时（首屏/路由切换/资源加载）展示进度条（类似 NProgress），并尽量模拟真实进度。

#### 实现要点

- 对静态资源：使用 `window.fetch` / XHR 拦截或 Resource Timing API 来估计进度（有限）。
- 对路由切换：在路由开始时 start(), 在请求成功/页面渲染完成时 done()。
- 模拟进度策略：快速到 80%-95%，等待真实事件完成再到 100%，并加上缓慢推进（decay）避免瞬间跳满。
- 使用 CSS 动画 + requestAnimationFrame 做平滑。

#### 简单实现（NProgress 风格）

```javascript
// progress.ts
let progress = 0;
let rafId: number | null = null;
function start() {
  progress = 0;
  tick();
  document.body.classList.add('show-progress');
}
function tick() {
  progress += Math.random() * 6; // 随机推进
  if (progress >= 95) progress = 95;
  update(progress);
  rafId = requestAnimationFrame(() => setTimeout(tick, 200));
}
function done() {
  if (rafId) cancelAnimationFrame(rafId);
  progress = 100;
  update(100);
  setTimeout(() => {
    document.body.classList.remove('show-progress');
    update(0);
  }, 300);
}
function update(val:number){ const el=document.getElementById('top-progress')!; el.style.width=val+'%'; }
export { start, done };
<!-- index.html -->
<!-- index.html -->
<div id="top-progress" style="position:fixed;left:0;top:0;height:3px;background:#1890ff;transition:width .2s"></div>
```

### 场景题 — web 应用中静态资源加载失败的降级处理

**目标**：当静态资源（JS/CSS/图片/字体）加载失败时优雅降级，保证核心功能和可用性。

#### 策略

1. **资源重试**（指数退避，最多 N 次）；
2. **备用 CDN / 本地备份**（先尝试主 CDN，失败后切换备选）；
3. **功能降级**：按优先级分离关键/非关键资源（critical CSS/JS 内联/延迟加载可选功能）；
4. **图片占位/矢量占位**：使用 `<img onerror=>` 替换为占位图或 SSG 的 Base64 占位；
5. **字体回退**：CSS 中定义备选字体，使用 `font-display: swap;`；
6. **监控/上报**：捕获 `window.onerror`, `resource timing`, 上报失败日志。

#### 代码 demo：资源加载重试与备用 URL

```javascript
async function loadScriptWithRetry(urls: string[], maxRetry = 3) {
  for (let u of urls) {
    for (let attempt = 0; attempt < maxRetry; attempt++) {
      try {
        await new Promise<void>((resolve, reject) => {
          const s = document.createElement('script');
          s.src = u;
          s.onload = () => resolve();
          s.onerror = () => reject(new Error('load fail'));
          document.head.appendChild(s);
        });
        return;
      } catch (e) {
        await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 200));
      }
    }
  }
  throw new Error("All script urls failed");
}

// 使用：优先 CDN1, CDN2, 本地
loadScriptWithRetry(["//cdn1/lib.js","//cdn2/lib.js","/static/lib.js"]).catch(err => {
  console.error("lib load failed", err);
  // 进入降级方案：隐藏相关功能、显示提示等
});
```

### 场景面试 — 前端水印功能（图片/页面/导出到 PDF 的水印）

**目标**：实现抗篡改、可配置的文本/图片水印，能在导出/截图中保留。

#### 思路

- 页面水印：使用 fixed/flexible 的重复背景层（CSS + Canvas 生成图片，设置 `pointer-events:none`）。
- 防移除：用多层水印或定期校正（定时检查 DOM）。
- 导出（PDF/截图）：在导出前把水印绘制到 canvas/PDF。

#### 示例（Canvas 生成图像作为背景）

```javascript
function createWatermarkDataURL(text: string) {
  const canvas = document.createElement('canvas');
  canvas.width = 300; canvas.height = 200;
  const ctx = canvas.getContext('2d')!;
  ctx.fillStyle = 'rgba(0,0,0,0.08)';
  ctx.font = '16px sans-serif';
  ctx.translate(150,100);
  ctx.rotate(-Math.PI/12);
  ctx.fillText(text, -50, 0);
  return canvas.toDataURL('image/png');
}

function applyWatermark(text: string) {
  const url = createWatermarkDataURL(text);
  const div = document.createElement('div');
  div.id = 'app-watermark';
  Object.assign(div.style, {
    position: 'fixed', left:0, top:0, right:0, bottom:0,
    pointerEvents:'none', backgroundImage:`url(${url})`, opacity:'0.8', zIndex: 999999
  });
  document.body.appendChild(div);
}
```

### 场景面试 — 大数运算（JS 精度问题、实现大整数/高精度浮点）

**目标**：处理精度丢失（JS Number）场景，支持加减乘除/四舍五入/大整数、任意精度小数。

#### 做法

- 使用 BigInt（原生）处理整数（但不支持小数）。
- 使用高精度库 `decimal.js`、`big.js`、`bignumber.js` 来处理小数场景。
- 或者实现字符串算术（手写大数加减乘），适合面试考题实现。

#### 简单字符串大整数加法（面试算法题）

```javascript
function addBigIntStr(a, b) {
  let carry = 0, res = [];
  a = a.split('').reverse(); b = b.split('').reverse();
  const n = Math.max(a.length, b.length);
  for (let i=0;i<n;i++){
    const x = parseInt(a[i]||'0',10);
    const y = parseInt(b[i]||'0',10);
    const s = x+y+carry;
    res.push(String(s%10));
    carry = Math.floor(s/10);
  }
  if (carry) res.push(String(carry));
  return res.reverse().join('');
}
```

### 场景面试 — 设计全站请求耗时统计工具（APM-lite）

**目标**：客户端采集全站请求耗时（XHR/Fetch/资源/路由变更）并上报到后端，便于性能分析。

#### 采集点

- XHR/Fetch 拦截（包装原生）；
- Resource Timing API（图片、css、js）；
- Navigation Timing / PerformanceObserver（first paint, first contentful paint）；
- 页面路由（SPA）– 记录路由开始/结束时间；
- 后端接收并聚合（时间窗口、聚合维度：url、status、用户、traceId）。

#### 前端核心实现（拦截 Fetch & XHR）

```javascript
// wrapFetch.ts
const originalFetch = window.fetch;
window.fetch = async function(...args) {
  const start = performance.now();
  try {
    const res = await originalFetch.apply(this, args);
    const duration = performance.now() - start;
    sendPerf({ url: args[0], type:'fetch', status: res.status, duration });
    return res;
  } catch (err) {
    const duration = performance.now() - start;
    sendPerf({ url: args[0], type:'fetch', status: 0, duration, error: String(err) });
    throw err;
  }
};

function sendPerf(payload: any) {
  // 用 beacon 优先
  if (navigator.sendBeacon) {
    const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
    navigator.sendBeacon('/perf/collect', blob);
  } else {
    fetch('/perf/collect', { method:'POST', body: JSON.stringify(payload), headers: {'Content-Type':'application/json'}});
  }
}
```

#### 后端聚合（思路）

- 接收事件并写入时序 DB（InfluxDB / ClickHouse / ES / Kafka），
- 聚合成接口平均耗时、P95、错误率，按 URL/TAG 分组，做告警。

### 场景面试 — 函数式编程（FP 在前端的应用）

**目标**：解释函数式编程概念并举例（纯函数、不可变数据、高阶函数、组合、柯里化、侧边效果最小化）。

#### 面试要点与示例

- 纯函数：相同输入返回相同输出，无副作用。
- 不可变：使用 `Object.freeze` / 结构性共享（immer）
- 高阶函数：`map`, `filter`, `reduce` 或自定义 `compose`
- 柯里化/组合：提高可复用性

```javascript
const compose = (...fns) => x => fns.reduceRight((v,f)=>f(v), x);
const add = a => b => a + b;
const double = x => x*2;
const incThenDouble = compose(double, add(1));
console.log(incThenDouble(2)); // (2+1)*2 = 6
```

#### 在前端的应用示例

- 数据转换流水线（normalize -> enrich -> filter）
- 事件处理链（pure handlers -> side-effect at edge）
- 状态管理策略（不可变 reducers）

### 场景面试 — 电影票选座功能（座位布局、并发抢占、锁座、支付完成释放）

**目标**：设计票选座功能，解决并发冲突、锁定、超时释放、支付一致性。

#### 关键设计

- 座位模型：`seat { id, status: available|locked|sold, lockedBy, lockedAt }`
- 锁座机制（短期锁，例如 5 分钟）：通过接口 `POST /seat/lock` 返回 lockId 或失败
- 乐观/悲观策略：悲观锁适合高并发（直接写 DB 标记 locked），乐观通过 compare-and-swap（version 或 SQL where status = available）
- 支付与最终确认：支付成功后 `POST /seat/confirm` 把 locked -> sold；如果超时或取消，自动解锁（后台定时任务或延迟队列）
- 并发策略：数据库事务或 Redis 分布式锁；使用消息队列确保幂等

#### 前端交互（示例）

```javascript
async function tryLockSeat(seatId) {
  const resp = await fetch(`/api/seat/lock`, { method:'POST', body: JSON.stringify({ seatId }), headers:{'Content-Type':'application/json'}});
  const data = await resp.json();
  return data; // { success: true/false, lockId, expiresAt }
}
```

#### 后端示例（伪代码）

```plsql
-- 悲观 SQL (Postgres)
BEGIN;
UPDATE seats SET status='locked', locked_by=$1, locked_at=now()
WHERE id=$2 AND status='available';
-- check affected rows == 1 then commit else rollback
COMMIT;
```

或 Redis：

```java
SETNX seat:{id} {lockInfo} EX 300
```

------

### 场景面试 — 图片性能优化方案

**目标**：降低页面图片的加载成本和渲染成本，提高 UX 和 CLS。

#### 关键措施

- 图片格式：WebP/AVIF > JPEG/PNG（按浏览器支持）。
- 响应式：`srcset` / `sizes` / `<picture>`。
- Lazy loading (`loading="lazy"` 或 IntersectionObserver)。
- 占位（LQIP / SVG 占位 / blur-up）。
- CDN + 缩放/裁剪服务：根据 breakpoints 生成合适尺寸。
- 图片压缩（lossy/lossless）、按需惰性解码 `decoding="async"`。
- 使用 `prefetch` 或 `preload` 关键图（首屏 hero）。
- 避免 layout shift：为图片预留宽高（或使用 aspect-ratio）。
- 精细处理背景图片和 icon（SVG sprite 或 icon font）。

#### 简单示例（picture + srcset + lazy）

```html
<picture>
  <source type="image/avif" srcset="cat-400.avif 400w,cat-800.avif 800w" sizes="(max-width:600px) 400px,800px">
  <source type="image/webp" srcset="cat-400.webp 400w,cat-800.webp 800w">
  <img src="cat-800.jpg" alt="cat" loading="lazy" width="800" height="450" decoding="async">
</picture>
```

### 场景题 — 移动端上拉加载 / 下拉刷新

**目标**：实现移动端上拉加载更多与下拉刷新体验（带阻尼效果、节流、避免冲突）。

#### 要点

- 用 touch 事件或 pointer events 实现下拉；要兼容浏览器默认下拉（在 WebView 中常禁用默认）。
- 下拉刷新：拖拽距离阈值（e.g. 60px）+ 回弹动画 + 状态机（idle -> pulling -> ready -> refreshing -> done）。
- 上拉加载：监听滚动到接近底部 `scroll` 或 IntersectionObserver（更优）。
- 避免冲突：在下拉时禁用滚动或在顶部检测才启下拉。

#### 简单实现（下拉刷新）

```javascript
let startY = 0, pulling = false;
const threshold = 60;
const el = document.getElementById('scrollWrap');
el.addEventListener('touchstart', (e) => {
  if (el.scrollTop === 0) { startY = e.touches[0].clientY; pulling = true; }
});
el.addEventListener('touchmove', (e) => {
  if (!pulling) return;
  const delta = e.touches[0].clientY - startY;
  if (delta > 0) {
    e.preventDefault();
    const dist = Math.min(delta / 2, 120);
    document.getElementById('ptr').style.transform = `translateY(${dist}px)`;
    // update UI status if > threshold
  }
});
el.addEventListener('touchend', async (e) => {
  if (!pulling) return;
  pulling = false;
  const endY = e.changedTouches[0].clientY;
  if (endY - startY > threshold) {
    // 执行刷新
    document.getElementById('ptr').classList.add('refreshing');
    await doRefresh();
    document.getElementById('ptr').classList.remove('refreshing');
  }
  document.getElementById('ptr').style.transform = '';
});
```

上拉加载用 IntersectionObserver 监听底部 sentinel 元素即可（节省资源，避免频繁 scroll 事件）。

### 场景面试 — 深度 SEO 优化（前端可做的 SEO）

**目标**：提高搜索引擎抓取和排名（技术和内容层面的结合）。

#### 前端可做项（面试重点）

- 服务端渲染（SSR）或预渲染（SSG）以确保搜索引擎抓取到完整 HTML（尤其是动态内容）。
- 语义化 HTML（正确 H1-H6，article, nav, main），结构化数据（schema.org JSON-LD）。
- 优化 meta（title/description）、Open Graph、canonical 链接、hreflang（多语言）。
- 页面性能（首屏速度、LCP、CLS）直接影响 SEO；使用 Lighthouse 指标优化。
- URL 设计：静态、友好、避免参数过多。
- robots.txt、sitemap.xml、XML sitemap 动态生成并提交到搜索引擎。
- 图片 lazy + `alt` 属性+ 图片 sitemap。
- 使用 `rel="next/prev"`、分页优化、避免重复内容（canonical）。
- 移动优先（移动端友好）和安全 (HTTPS)。

#### 代码示例：JSON-LD schema

```javascript
<script type="application/ld+json">
{
  "@context":"https://schema.org",
  "@type":"Article",
  "headline":"如何优化图片性能",
  "author": { "@type":"Person", "name":"大伟" },
  "datePublished":"2025-10-15",
  "image":"https://example.com/cover.jpg"
}
  </script>
```

### 无感刷新Token：如何做到让用户“永不掉线”


没有什么比在用户操作得正嗨时，突然提示“登录已过期，请重新登录”的提示更让人沮丧的了。这种突兀的中断不仅破坏了用户体验，甚至可能导致未保存的数据丢失。

然而，我们都知道，出于安全考虑，用于身份验证的 `Token`（通常是 `Access Token`）必须有较短的有效期。那么，我们如何在保证安全的前提下，创造一种“永不掉线”的丝滑体验呢？

#### **问题的根源：Access Token 的“天生矛盾”**

首先，我们要理解为什么需要刷新 Token。

我们通常使用 `Access Token` 来验证用户的每一次 API 请求。为了安全，`Access Token` 的生命周期被设计得很短（例如 30 分钟或 1 小时）。如果有效期太长，一旦泄露，攻击者就能在很长一段时间内冒充用户进行操作，风险极高。

这就产生了一个矛盾：

- **安全性要求**：`Access Token` 有效期要短。
- **用户体验要求**：用户不想频繁地被强制重新登录。

为了解决这个矛盾，**`Refresh Token`** 应运而生。

#### **核心理念：双 Token 认证系统**

无感刷新机制的核心在于引入了两种类型的 Token：

1. **`Access Token`（访问令牌）**

2. - **用途**：用于访问受保护的 API 资源，附加在每个请求的 `Header` 中。
   - **特点**：生命周期短（如 1 小时），无状态，服务器无需存储。
   - **存储**：通常存储在客户端内存中（如 Vuex/Redux），因为需要频繁读取。

3. **`Refresh Token`（刷新令牌）**

4. - **用途**：当 `Access Token` 过期时，专门用于获取一个新的 `Access Token`。
   - **特点**：生命周期长（如 7 天或 30 天），与特定用户绑定，服务器需要安全存储其有效性记录。
   - **存储**：必须安全存储。**最佳实践是存储在 `HttpOnly` Cookie 中**，这样可以防止客户端 JavaScript 脚本（如 XSS 攻击）读取它。

既然如此，为何不直接使用 `Refresh Token` 呢？

`Access Token` 通常是无状态的，服务器无需记录它，也导致 JWT 无法主动吊销，而 `Refresh Token` 是**有状态的**，服务器需要一个列表（数据库中的“白名单”或“吊销列表”）来记录哪些 `Refresh Token` 是有效的，当用户更改密码、或从某个设备上“主动登出”时，服务器端可以主动将对应的 `Refresh Token` 设为无效。

#### **无感刷新的详细工作流**

下面是这个“魔法”发生的具体步骤：

1. **首次登录**：用户使用用户名和密码登录。服务器验证成功后，返回一个 `Access Token` 和一个 `Refresh Token`。

2. **正常请求**：客户端将 `Access Token` 存储起来，并在后续的每次 API 请求中，通过 `Authorization` 请求头将其发送给服务器。

3. **Token 过期**：当 `Access Token` 过期后，客户端再次用它请求 API。服务器会拒绝该请求，并返回一个特定的状态码，通常是 `401 Unauthorized`。

4. **拦截 401 错误**：客户端的请求层（如 Axios 拦截器）会捕获这个 `401` 错误。此时，它不会立即通知用户“你已掉线”，而是**暂停**这个失败的请求。

5. **发起刷新请求**：拦截器使用 `Refresh Token` 去调用一个专门的刷新接口（例如 `/api/auth/refresh`）。

6. **处理刷新结果**：

7. - **刷新成功**：服务器验证 `Refresh Token` 有效，生成一个新的 `Access Token`（有时也会返回一个新的 `Refresh Token`，这被称为“刷新令牌旋转”策略，可以提高安全性），并将其返回给客户端。
   - **刷新失败**：如果 `Refresh Token` 也过期了或无效，服务器会返回错误（如 `403 Forbidden`）。这意味着用户的登录会话彻底结束。

8. **重试与终结**：

9. - **若刷新成功**：客户端用新的 `Access Token` **自动重发**刚才失败的那个 API 请求。用户完全感觉不到任何中断，数据操作无缝衔接。
   - **若刷新失败**：客户端清除所有认证信息，强制用户登出，并重定向到登录页面。

#### **实战演练：使用 Axios 拦截器实现无感刷新**

`Axios` 的拦截器是实现这一流程的完美工具。下面是一个完整且考虑了并发问题的实现方案。

**1. 创建 Axios 实例**

首先，我们创建一个单独的 Axios 实例，方便统一管理。

```js
// a-pi/request.js
import axios from 'axios';

const service = axios.create({
 baseURL: '/api',
 timeout: 10000,
});

// 请求拦截器
service.interceptors.request.use(
 config => {
    // 在发送请求之前，从 state management (e.g., Vuex/Pinia/Redux) 获取 token
    const accessToken = getAccessTokenFromStore(); 
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
 error => {
    return Promise.reject(error);
  }
);
```

**2. 核心：响应拦截器**

这是实现无感刷新的关键。



```js
// a-pi/request.js (续)

// 用于刷新 token 的 API
import { refreshTokenApi } from './auth'; 

let isRefreshing = false; // 控制刷新状态的标志
let requests = []; // 存储因 token 过期而挂起的请求

service.interceptors.response.use(
 response => response, // 对成功响应直接返回
 async error => {
    const { config, response: { status } } = error;
    
    // 1. 如果不是 401 错误，直接返回错误
    if (status !== 401) {
      return Promise.reject(error);
    }

    // 2. 避免重复刷新：如果正在刷新 token，将后续请求暂存
    if (isRefreshing) {
      return new Promise(resolve => {
        requests.push(() => resolve(service(config)));
      });
    }

    isRefreshing = true;

    try {
      // 3. 调用刷新 token 的 API
      const { newAccessToken } = await refreshTokenApi(); // 假设 refresh token 通过 HttpOnly cookie 自动发送

      // 4. 更新本地存储的 access token
      setAccessTokenInStore(newAccessToken);
      
      // 5. 重试刚才失败的请求
      config.headers['Authorization'] = `Bearer ${newAccessToken}`;
      
      // 6. 重新执行所有被挂起的请求
      requests.forEach(cb => cb());
      requests = []; // 清空队列
      
      return service(config); // 返回重试请求的结果
    } catch (refreshError) {
      // 7. 如果刷新 token 也失败了，则执行登出操作
      console.error('Unable to refresh token.', refreshError);
      logoutUser(); // 清除 token，重定向到登录页
      return Promise.reject(refreshError);
    } finally {
      isRefreshing = false;
    }
  }
);

export default service;
```

**代码解析：**

- **并发处理**：`isRefreshing` 标志和 `requests` 数组是关键。当第一个 `401` 错误触发刷新时，`isRefreshing` 变为 `true`。后续在刷新完成前到达的 `401` 请求，都会被推进 `requests` 队列中挂起，而不是重复发起刷新请求。当刷新成功后，再遍历队列，依次执行这些被挂起的请求。
- **原子操作**：通过这种“加锁”机制，确保了刷新 Token 的操作是原子的，避免了资源浪费和潜在的竞态条件。
- **优雅降级**：当 `Refresh Token` 也失效时，系统会执行 `logoutUser()`，进行清理工作并引导用户重新登录，这是一个优雅的失败处理方案。

无感刷新 Token 机制是现代 Web 应用提升用户体验的“标配”。它将身份验证的复杂性隐藏在后台，为用户提供了一个流畅、不间断的操作环境。

实现这一机制，不仅仅是写几行代码，更是对认证流程、安全性和用户体验三者之间平衡的深刻理解。
