跳转到主要内容

面试题库

前端面试真题汇总 2026

HTML+CSS、移动端、JS 基础、Vue、React、工程化、性能优化、Node.js、小程序、场景题,共 296 题。

  • 前端
  • 面试

2026 前端面试真题汇总

来源:resource/面试题.zip → 04-御码2026前端面试真题汇总.md

HTML + CSS

1- 标签语义化的意义

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

2- 写页面结构应该注意什么

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

3- HTML5 新特性

  1. 新的语义化元素:article 、footer 、header 、nav 、section
  2. 表单增强,新的表单控件:calendar 、date 、time 、email 、url 、search
  3. 新的 API:音频(用于媒介回放的 video 和 audio 元素)、图形(绘图 canvas 元素)
  4. 新的 API:离线,通过创建 cache manifest 文件,创建应用程序缓存
  5. 新的 API:本地存储,localStorage-没有时间限制的数据存储,sessionStorage-session 数据存储(关闭浏览器窗口数据删除)
  6. 新的 API:实时通讯,设备能力

4- 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. 媒体查询,多栏布局

5- 盒子模型

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

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

6- 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 区域之中)
    • 自适应两栏布局
    • 可以阻止元素被浮动元素覆盖

7- 选择器优先级

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

8. 去除浮动影响,防止父级高度塌陷

  • 通过增加尾元素清除浮动
  • :after / <br> : clear: both
  • 创建父级 BFC
  • 父级设置高度
  • link功能较多,可以定义 RSS,定义 Rel 等作用,而@import只能用于加载 css
  • 当解析到link时,页面会同步加载所引的 css,而@import所引用的 css 会等到页面加载完才被加载
  • @import需要 IE5 以上才能使用
  • link可以使用 js 动态引入,@import不行

10. CSS 预处理器(Sass/Less/Postcss)

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

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

11. 单行文本溢出

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

12. 多行文本溢出

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

13. 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属性:定义了在分配多余的空间,项目占据的空间。

14. 圣杯布局

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

15. 双飞翼布局

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

移动端

1. 基础知识

  • 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标签实现

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

2. 适配

1.viewport 适配

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

2.rem适配

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

3.vw适配

vw和vh是两个相对单位

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

4.1px物理像素边框

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

方法一

使用媒查询:

@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倍

var viewport = document.querySelector('meta[name=viewport]')
var scale = 1 / window.devicePixelRatio
viewport.setAttribute('content', 'width=device-width,initial-scale=' + scale);
  1. 重新设置根元素字体
var fontSize = parseInt(document.documentElement.style.fontSize);
document.documentElement.style.fontSize = fontSize * window.devicePixelRatio + 'px'

3. 移动端事件

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

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

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

5. 点击穿透

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

    方法一:阻止默认行为

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

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

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

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

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

总结

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

1- var / let / const 的区别

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

示例:

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

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

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

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 会先强制转换)。

示例:

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

3- 值类型 vs 引用类型

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

示例:

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

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

4- 深拷贝 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):

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;
}

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

  • 遍历/映射:

    • 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):
function mapByReduce(arr, fn) {
  return arr.reduce((acc, cur, idx) => {
    acc.push(fn(cur, idx, arr));
    return acc;
  }, []);
}

6- 对象遍历方式对比(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)。

示例:

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

7- 隐式类型转换的坑

  • 规则:

    • + 若一方是字符串则执行字符串拼接;否则数值化(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。
  • 面试点:能用具体例子说明 == 的行为;对象到原始值转换顺序;+ 的优先字符串拼接或数字转换问题。

示例:

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

8- == 和 === 的核心考点

  • ===:严格相等,不做类型转换(若类型不同,直接 false)。

  • ==:抽象相等,会在许多情况下进行类型转换:

    • null == undefined -> true
    • 如果类型不同,按表规则转换:字符串与数字 -> 字符串转数字;布尔 -> 转数字;对象 -> 转原始值(调用 valueOf / toString)。
  • 面试点:要能列出若干典型 == 的例子并解释原因(尤其 null/undefined、布尔与字符串、对象与原始值比较),以及推荐在业务代码中尽量使用 ===。

9- 执行上下文(execution context)和作用域链(scope chain)

  • 执行上下文 包括:变量环境(variable environment)、词法环境(lexical environment,环境记录 + 外部引用)、this 绑定和作用域链。

  • 创建阶段(creation):

    • 为函数/全局创建绑定(变量提升、函数提升、参数对象)。
  • 执行阶段(execution):执行代码,变量赋值,函数引用分配。

  • 作用域链:当访问一个标识符时,JS 从当前执行上下文的词法环境开始查找,找不到则向上查找直至全局(或 null)。

  • 面试点:区别词法作用域与动态作用域、变量提升的细节、let/const 和 TDZ、如何解释闭包是如何访问外层变量的。

示例(展示作用域链):

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

10- JS 中的 this 究竟指向谁?

  • 四条规则(优先级):
    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 创建的实例为准);箭头函数不能作为构造器。

示例:

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

11- 闭包(closure)到底是什么?如何判断?

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

示例:

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

12- 高阶函数和柯里化(应用场景)

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

示例(柯里化):

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

13- 节流(throttle) vs 防抖(debounce)

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

防抖:

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

节流(时间戳版):

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

14- bind、call、apply 的底层原理

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

示例(简化实现):

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;
};

15- 立即执行函数(IIFE)是怎么工作的?

  • 目的:创建独立作用域,避免污染全局,常用于早期模块化或块级封装。

  • 写法:

    • (function(){ /*...*/ })();
    • (() => { /*...*/ })();
    • !function(){ /*...*/ }();(有趣但不常用)
  • 注意:前面可能需要分号(防止与前一个未结束的语句造成解析错误)。

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

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

17- 手写 bind 实现(更完整版)

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;
};

18- 手写柯里化函数

  • 支持一次或多次传参,直到参数数量满足原函数长度:
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

19- 函数组合、记忆化(memoization)

  • 组合(compose/pipe):
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
  • 记忆化:利用 Map 缓存计算结果(注意:参数是对象时需要特殊处理或使用 WeakMap):
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;
  };
}
  • 面试点:记忆化适用于纯函数,注意缓存失效/内存泄漏问题以及参数可序列化性。

20- setTimeout 和 setInterval 的陷阱

  • setInterval 可能导致任务重叠(任务执行时间超过间隔),建议用递归 setTimeout 来确保间隔是任务完成后的固定延迟。
  • 最小延迟限制(浏览器或 Node 在后台页或某些环境会限制最小延迟)。
  • 清除需要 clearTimeout/clearInterval。
  • 计时精度:不保证精确(受事件循环阻塞影响)。
  • 示例(安全的间隔):
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; };
}

21- 事件循环(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。

经典示例:

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

22- Promise 基本语法 + 错误捕获

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

示例:

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

23- async / await 的底层运行机制

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

伪转译(概念):

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);
  });
}

24- 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(包含所有错误)。
  • 面试点:选择哪个取决于业务需求(是“全成功”还是“先到先得”或“至少一个成功”)。

示例:

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

25- 手写一个简化版 Promise(核心功能:then 链式)

下面实现为简化版,支持链式 .then,基本的 resolve/reject,Promise 值穿透与 thenable 兼容(简化版,未实现所有 edge case):

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。

26- 实战:用 Promise 重写 setTimeout 任务队列

  • 需求:按顺序执行一组带延迟的任务(每个任务按指定延迟执行,前一个完成才开始下一个)。
  • 实现思路:用 Promise 链或 async/await 顺序调度。

示例(任务队列类):

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:
const wait = ms => new Promise(res => setTimeout(res, ms));
await wait(1000);

27- 实战:Async / await 错误处理的 3 种方式

  1. try / catch(最直观):
try {
  const res = await foo();
} catch (err) {
  // 处理
}
  1. 对 Promise 使用 **.catch**(适用于并行):
const p = foo();
p.catch(err => { /* 处理 */ });
  1. 返回 [err, result] 的封装(避免 try/catch):
const to = (p) => p.then(data => [null, data]).catch(err => [err]);
const [err, data] = await to(foo());
if (err) { /* 处理 */ }

28- “异步题大汇总”:输出顺序、陷阱解析(几个常见面试题)

  • 经典例子(解释输出):
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// 输出: start, end, promise, timeout
  • 更复杂(微任务链):
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)等。

29- 什么是原型?什么是原型链?

  • 原型(prototype):每个函数(构造函数)都有 prototype 对象,实例对象通过 __proto__(或 Object.getPrototypeOf)指向构造函数的 prototype。属性查找会沿着这条链向上查找,这就是原型链。
  • 面试点:区分 prototype(函数的属性,用于实例的原型)与 __proto__(实例的指针);Object.prototype 的 __proto__ 是 null(链的终点)。

示例:

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

30- 构造函数与 new 的机制

new 的执行过程(面试常考)简述:

  1. 创建一个新对象:obj = {}。
  2. 将新对象的 __proto__ 指向构造函数的 prototype(obj.__proto__ = Fn.prototype)。
  3. 将构造函数内部 this 绑定到新对象并执行构造函数:Fn.call(obj, ...args)。
  4. 若构造函数返回一个对象(非 null 且类型为 object 或 function),则返回该对象;否则返回 obj。
  • 面试点:要写出 new 的手写实现(见第 31 条)。

31- 手写 new 的实现逻辑

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');

32- instanceof 背后的原理

  • 原理:判断构造函数的 prototype 是否出现在对象的原型链上。
  • 手写实现:
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 在类型检测上的作用。

33. Object.create 是怎么实现继承的?

  • Object.create(proto) 本质上创建了一个新的对象并将它的内部 [[Prototype]] 指向 proto。
  • 简单 polyfill:
function objectCreate(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}
  • ES6 中可直接用 Object.create(proto)。Object.create(null) 创建无原型的纯字典对象。

34. class 是语法糖吗?背后发生了什么?

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

示例(等价思想):

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

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

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

36- 说说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() 专门判断数组

37- 说说常见的数组方法

  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

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

38- 说说你对作用域与作用域链的理解

  • 作用域

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

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

39- 说说变量提升与函数提升

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

40- 区分执行函数定义与执行函数

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

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

1- 什么是 Vue 中的 slot?它有什么作用?

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

常见类型:

  • 默认插槽(unnamed slot)
  • 具名插槽(named slot)
  • 作用域插槽 / 作用域插槽(scoped slot)——子组件向插槽提供数据,父组件接收并渲染

示例(Vue 3 / Vue 2 语法一致):

子组件 BaseCard.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>

父组件使用:

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

    主体内容在这里

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

作用域插槽示例(子提供数据,父使用): 子组件 ListProvider.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>

父组件:

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

2- 在 Vue 渲染模板时,如何保留模板中的 HTML 注释?

要点:Vue 默认会移除模板中的 HTML 注释(<!-- -->)。如果需要保留注释,一般是调试或特定场景。

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

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

代码示例(手动插入注释):

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

3- Vue 计算属性的函数名和 data 中的属性可以同名吗?为什么?

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

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

示例(错误写法):

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

正确做法:使用不同命名或改为 computedCount。

4- Vue 的 v-show 和 v-if 有什么区别?使用场景分别是什么?

要点:

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

选用建议:

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

示例:

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

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

5- 如何在 Vue 中使用 defineAsyncComponent 实现异步组件加载?

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

Vue 3 示例:

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()
    }
  }
})

模板中:

<template>
  <AsyncComp />
</template>

Vue 2 示例(异步工厂):

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

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

要点(面试回答要点):

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

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


7- 在 Vue 项目中,你通常如何组织和管理组件?请描述一下你的组件化开发思路

原则:

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

示例目录结构:

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

组件开发流程(面试回答的流程化思路):

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

8- 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):

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

示例(Composition API):

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

9- 谈谈你对 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):

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

v-model 在父子组件传值上的示例(Vue 3): 父组件:

<Child v-model="name" />

子组件:

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

10- 在 Vue 中,如何进行路由管理?你是否使用过 Vue Router?

要点:

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

Vue 3 + Vue Router 4 示例:

// 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

挂载:

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

路由守卫示例(全局):

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

11- 对于 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 场景注意状态注水/反序列化

12- 请你说说 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 示例:

// 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) }
  }
})

13- 对于 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):

<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>

14- 请分享一些你在 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)

示例:路由懒加载:

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

15- 如果要在 Vue 项目中集成第三方库或插件,你通常会采取哪些步骤?

流程:

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

示例(注册插件):

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

16- 请描述一次你在 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,用户感知显著提升。

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

要点:

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

示例(Composition API):

// 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 }
}

18- Vue 组件之间的通信方式有哪些?

列表:

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

示例(父子):

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

19- 为什么 data 属性是一个函数而不是一个对象?

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

示例:

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

20- 动态给 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):

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

21- 说说你对 Vue 的 mixin 的理解,有什么应用场景?

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

应用场景:

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

替代方案(更推荐):

  • Composition API(useXxx hooks)更明确、可组合、类型安全

示例 mixin:

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

使用:

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

22- Vue 常用的修饰符有哪些,有什么应用场景?

事件修饰符(常用):

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

按键修饰符:

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

表单修饰符:

  • .lazy:失去焦点或 change 时更新(默认 input 实时)
  • .number:把输入转为 Number
  • .trim:去掉首尾空格

指令修饰符:

  • v-bind 的 .prop, .camel(用于属性名转换)

示例:

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

23- Vue 的 computed 和 watch 有什么区别?

要点:

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

使用建议:

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

示例:

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

24- 有写过自定义指令吗?自定义指令的应用场景有哪些?

常见场景:

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

Vue 3 自定义指令示例(**v-focus**):

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

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

模板使用:

<input v-focus />

25- SPA 首屏加载速度慢的怎么解决?

策略汇总:

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

26- Vue 3 有了解过吗?能说说跟 Vue 2 的区别吗?

核心差异:

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

27- Vue 3 为什么使用 Proxy 拦截数据?

要点:

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

28- Vue 3 新增特性

主要新增(面试摘要):

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

29- 如何看待 Composition API 和 Options API

对比与看法:

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

30- Vue 3.0 编译做了哪些优化?

要点:

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

31- watch 和 watchEffect 的区别?

要点(Vue 3):

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

示例:

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

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

32- 如何理解 reactive、ref、toRef 和 toRefs?

要点:

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

示例:

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

33- Vue 的设计哲学 & 核心理念

答案要点(面试话术)

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

可补充谈点

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

示例(谈话示例)

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

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

答案要点

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

面试话术

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

Demo

<!-- 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>

35- Composition-API 基础语法讲解

重点 API

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

示例(组合使用)

<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>

36- Vue 的响应式陷阱

常见陷阱(面试应答点)

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

修复示例

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,保持响应

37- Vue 响应式底层原理 — Proxy(简述)

核心要点

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

示例(手写极简版本,面试中可以白板/口述)

// 极简依赖收集示意(非完整实现)
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;
}
});
}

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

要点

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

代码示例:computed 实现核心(伪代码)

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(); } };
}

39- Vue 响应式更新 vs React 状态更新

对比要点(面试回答结构化)

  • Vue:基于依赖追踪,细粒度更新(只更新受影响的组件/模板片段);响应式对象追踪属性级别。

  • React:基于不可变状态(setState / useState)触发组件重新渲染,组件内所有依赖都会重新执行(整体函数组件重新运行),需借助 memo/pureComponent 做局部优化。

  • 优点/缺点:

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

面试话术

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

示例对比(伪示意)

  • Vue:
<p>{{ user.name }}</p> <!-- 只有 name 被依赖,修改 age 不会重新渲染该绑定 -->
  • React:
function Profile({ user }) {
  return <p>{user.name}</p>; // 只要父组件 setState 导致 user reference 变化,Profile 会重新渲染
}

40- Vue 常用的生命周期函数

列表(setup 环境下)

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

示例

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

面试话术

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

41- Vue 的副作用清理逻辑

要点

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

示例:watchEffect 清理

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 };
}

42- 模板语法 — 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。

示例

<!-- 不推荐 -->
<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 自定义组件

<!-- 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>

43- Vue 的通信基础

方式

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

面试话术

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

代码示例(父子通信)

<!-- 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>

44- Provide-Inject 的用法

要点

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

示例

// 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 };
}

45- 插槽 slot 与具名插槽

要点

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

示例

<!-- 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>

46- 插槽作用域(Scoped Slots)

要点

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

示例

<!-- 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>

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

要点

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

示例

// 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');

48- 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 搭配更稳)

示例

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

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

49- Vue-Router 的路由守卫

类型

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

示例:全局守卫做鉴权

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();
});

50- Vue-Router 的路由元信息

要点

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

示例

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

51- Vue 何时需要全局状态

判断点(面试回答)

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

面试话术

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

52- Pinia 基本用法 — store

示例:简单 store

// 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 };
    }
  };

53- Pinia 三大核心概念

三大要点

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

面试话术

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

54- Pinia — Store 的响应式解构(常见问题)

问题

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

正确做法

  • 使用 storeToRefs 把 store 的 state -> refs,或直接在模板中使用 store.count。
import { storeToRefs } from 'pinia';
const store = useCounterStore();
const { count } = storeToRefs(store); // count 是 ref,保持响应性

55- Pinia 最小可变点原则

要点

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

面试话术

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

56- Vue 的自定义指令

用途

  • 操作 DOM(聚焦、悬浮提示、长按等),需要直接 DOM 操作时用指令。

API

  • beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted。

示例(v-focus)

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

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

  // 使用
  <input v-focus />

57- 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 复用。”

58- 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 最小移动策略,是面试中的高分点。”

59- nextTick 的作用是什么?

要点

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

示例

<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>

60- 区别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(控制器)接收用户提交的请求参数, 操作数据库生成动态数据并产生模型对象

61- Vue.use()做了什么

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

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

1- 如何理解 UI = f(state) 这个公式?

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

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

代码 demo:

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>
    </>
  );
}

2- 虚拟 DOM(Virtual DOM)是什么?它解决了哪些实际问题?

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

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

代码 demo(手写简化版本的虚拟 DOM diff):

// 极简:虚拟节点表示
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'));

3- 说说你对 JSX 的理解

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

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

代码 demo:

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

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

4- 函数组件和类组件的本质区别是什么?

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

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

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

代码 demo(同功能对比):

// 类组件
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>;
}

5- 为什么 React 中 props 不可变(immutable)?

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

代码示例(错误示范 + 正确做法):

// 错误(不要直接修改 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

6- React 的 Fiber 架构主要是为了解决什么问题?

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

面试要点:说明两个阶段(reconciliation 与 commit),Fiber 如何让中断与恢复成为可能,以及它对 useTransition/并发渲染等特性的支撑作用。LogRocket Blog+1

简化示意代码(伪代码展示任务分片):

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

7- React 中的 key 属性有什么作用?

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

代码 demo:

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

8- React 的事件机制和合成事件是如何工作的?

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

代码 demo:

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

9- 受控组件(controlled)和非受控组件(uncontrolled)有什么区别?

解释:

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

代码 demo(对比):

// 受控
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>
  </>;
}

10- 为什么在 React 中“组合优于继承”?

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

代码 demo(组合示例):

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

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

11- React 的严格模式(StrictMode)有什么作用?

解释:<StrictMode> 在开发模式下启用额外检查与警告(并不影响生产构建),如:

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

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

代码 demo:

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

12- useState 的更新是异步的吗?

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

代码 demo(说明):

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

13- 使用 useState 的函数式更新方式能带来哪些好处?

解释:函数式更新(setState(prev => newVal))的优势:

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

代码 demo:

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

14- “状态提升”这种模式有哪些优缺点?

解释:

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

代码 demo(状态提升):

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

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

15- useReducer 相比 useState 优势体现在哪?

解释:

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

代码 demo(计数器):

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>
    </>
  );
}

16- 如何优化因 useContext 引起的性能问题?

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

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

代码 demo(useMemo 防止不必要重渲染):

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>;
}

17- 说一下 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** 推荐):

// 使用 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());

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

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

代码 demo(使用 Immer 的写法 vs 等价不可变写法):

// 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;
}

19- Redux 的中间件(Middleware)机制是如何工作的?

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

代码 demo(自定义 logger 中间件):

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));

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

解释 & 要点:

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

代码 demo(Zustand 简单示例):

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>;
}

21- 像 SWR 或 React Query 这样的库,它们主要解决了什么问题?

解释:这些库专注于客户端数据获取(fetching)与缓存,解决了常见问题:

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

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

代码 demo(React Query):

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>;
}

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

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

代码 demo(Signal 风格伪代码):

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

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

解释(关键规则):

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

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

代码 demo(错误示范与修正):

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

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

24- useEffect 的执行时机具体是什么时候?它和 useLayoutEffect 有什么核心区别?

解释:

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

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

代码 demo:

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

25- useEffect 依赖项有什么用?

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

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

代码 demo:

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

26- 在 useEffect 中,如何正确地处理异步请求并避免竞态条件(race condition)?

解释 & 实践:

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

代码 demo(使用 AbortController):

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>;
}

27- 什么时候应该使用 useCallback 和 useMemo?如果滥用它们会带来什么后果?

解释:

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

面试要点:先测(profile),只在确实存在性能问题或避免不必要重新渲染时使用。

代码 demo:

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

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

28- useRef 有哪些常见的应用场景?

解释 & 场景:

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

代码 demo(保存上一次值):

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

29- forwardRef 和 useImperativeHandle 怎么用?

解释:

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

代码 demo(暴露 focus API):

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>;
}

30- useId 如何解决 SSR 场景下的 ID 冲突和可访问性问题?

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

代码 demo:

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

31- useTransition 和 useDeferredValue 的使用和区别?

解释:

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

代码 demo(对比):

// 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

32- React18 中的自动批处理(Automatic Batching)如何工作?

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

代码 demo(示例):

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

33- React19 中的 Activity 组件与 Suspense 有什么区别?

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

代码 demo(伪示意):

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 以官方文档为准。

34- React19 中的 use Hook 如何简化异步数据处理?

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

示意代码:

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

参见对 use() 的文章与示例(社区/官方解读)。Medium

35- 说一下 React19 对表单处理的改进:useActionState 和 useFormState

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

代码 demo(示意):

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>
  );
}

36- React19 中如何直接在组件中渲染 meta 标签和样式表?

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

代码 demo(示意):

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

37- React19 的 ref 作为 prop 传递有什么优势?

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

代码 demo(React 19 风格):

// 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 中该用法被支持。

38- React 是什么?

React是一个由 Facebook 开发和维护的开源 JavaScript 库,用于构建用户界面,特别是单页面应用程序(SPA)。以下是 React 的主要特点和概念:

  1. 虚拟 DOM:React 使用一个叫做虚拟 DOM 的内存中数据结构来跟踪实际 DOM 的变化。当组件的状态改变时,React 会创建一个新的虚拟 DOM 树并与之前的树进行比较,然后决定如何以最有效的方式更新实际的 DOM。
  2. 声明式编程:在 React 中,你只需描述 UI 应该如何根据不同的状态显示,而不是描述如何进行变化。这使得代码更容易理解和预测。
  3. JSX:React 引入了 JSX(JavaScript XML)语法,允许开发者在 JavaScript 代码中写入类似 HTML 的标记。这种结合使得 UI 逻辑更加直观。
  4. 组件化:React 以组件为中心,让开发者能够构建复杂的 UI 从小到大,通过组合不同的可重用组件。
  5. 单向数据流:React 组件通过 props 接收数据并显示它们,当数据发生变化时,组件会重新渲染。这种单向数据流使得状态管理更为清晰和可预测。
  6. 可与各种工具和库集成:尽管 React 专注于 UI,但它可以与各种状态管理工具(如 Redux 或 MobX)、路由库(如 React Router)以及其他前端工具和库一起使用。
  7. 跨平台:除了 Web 开发,Facebook 还推出了 React Native,它允许开发者使用相同的 React 概念来构建原生移动应用程序。

39- 虚拟 DOM 的工作原理是什么?

虚拟 DOM(Virtual DOM)是 React 中的核心概念之一。为了理解它,首先需要知道直接操作真实的 DOM 是代价高昂的,因为这可能引起浏览器的重排和重绘,导致性能瓶颈。虚拟 DOM 旨在最小化与真实 DOM 的交互次数,从而提高性能。 以下是关于 React 中的虚拟 DOM 的详细解释:

  1. 定义:虚拟 DOM 是对真实 DOM 的轻量级表示。它是一个 JavaScript 对象,映射了真实 DOM 的结构和属性。
  2. 工作机制:
    • 当 UI 的某个部分发生变化时(例如,因为用户输入或新数据),React 会创建一个新的虚拟 DOM 树。
    • 接着,React 会将新的虚拟 DOM 树与上一个版本的虚拟 DOM 树进行比较,这一步被称为“差异化”(diffing)。
    • 通过这一比较过程,React 能够确定真实 DOM 需要进行哪些最小的修改以反映新的 UI 状态。
    • 最后,React 将这些变化批量应用到真实的 DOM,这一步被称为“重新渲染”(reconciliation)。
  3. 优势:
    • 性能提高:通过批量更新真实 DOM,并只修改必要的部分,React 可以避免昂贵的 DOM 操作和不必要的重排/重绘。
    • 编程简化:开发者不必关心如何高效地更新 DOM。他们只需声明 UI 在各种状态下的外观,而 React 负责如何实现这一过程。
    • 跨平台:由于虚拟 DOM 只是 JavaScript 对象,因此 React 可以在不同环境中重复使用相同的逻辑,例如在 React Native 中。
  4. 局限性:
    • 虽然虚拟 DOM 提高了性能,但它并不是解决所有问题的银弹。在某些情况下,对真实 DOM 的直接操作可能更快。
    • 差异化和重新渲染仍然需要计算资源。对于大型应用或频繁的更新,开发者可能需要使用一些性能优化技巧。 总之,虚拟 DOM 是 React 如何快速和高效地更新 UI 的核心机制。通过将复杂的 DOM 更新逻辑抽象为简单的 JavaScript 运算,React 提供了一个强大且高性能的方法来构建动态 web 应用。

40- 为什么React要使用 JSX?

React 使用 JSX 为了提供一种更加直观、声明式的方式来描述 UI,并且在编写 UI 代码时能够保持 JavaScript 的所有功能。

核心概念:

  1. 声明式语法: JSX 提供了一种看起来很像 HTML 的语法,让我们能够直观地描述 UI 的外观和结构。这使得代码更加可读和易于维护。
  2. 组件化: 通过 JSX, 我们可以直接在 JavaScript 中定义组件,这使得 UI 的复用、测试和关注点分离变得简单。
  3. 整合能力: 由于 JSX 本质上是 JavaScript,我们可以在其中插入任何有效的 JavaScript 表达式。这为 UI 的创建提供了巨大的灵活性。

方案对比:

  1. 纯 JavaScript: 在没有 JSX 的情况下,UI 可以使用纯 JavaScript 来构建。但这样会使代码变得冗长和难以阅读,尤其是对于复杂的 UI 结构。与此相反,JSX 提供了一种简洁、易读的方式来定义 UI。
  2. 模板语言: 传统的前端框架经常使用特定的模板语言来描述 UI。虽然这些模板可以是声明式的,但它们通常限制了你可以使用的逻辑。与之相反,JSX 允许你在描述 UI 时使用完整的 JavaScript 功能。
  3. 字符串拼接: 直接使用字符串拼接 HTML 是不安全的,因为它容易受到跨站脚本攻击(XSS)。而 JSX 在编译时会自动进行安全转义,从而防止这种攻击。 结论: React 采用 JSX 是为了结合 HTML 的直观性和 JavaScript 的强大功能,提供一种更加有效、安全和声明式的方式来构建用户界面。

41- 类组件与函数组件有什么区别?

什么是 React 组件?

React 组件是独立、可重用的代码片段,用于描述 UI 的一部分。组件可以是简单的 UI 元素(如一个按钮)或包含其他组件的复杂容器。

共同点:

  1. 渲染 UI: 无论是类组件还是函数组件,它们的主要目标都是渲染 UI。
  2. 接收 props: 两者都可以接收 props,并依据 props 渲染 UI。
  3. 组件生命周期: 在引入 Hooks 之前,只有类组件能使用完整的生命周期方法。但现在,通过使用 useEffect Hook,函数组件也可以模拟大多数生命周期行为。

不同点:

  1. 编程思想:
    • 类组件: 面向对象编程 基于 ES6 的类语法,需要理解 JavaScript 的 this 绑定。
    • 函数组件: 函数式编程 基于函数,更简洁,并且更易于理解。
  2. 使用场景:
    • 类组件: 当需要使用 state 或生命周期方法时。
    • 函数组件: 对于简单的组件或当你想利用 Hooks。
  3. 特有功能:
    • 类组件: 具有完整的生命周期方法和错误边界处理。
    • 函数组件: 可以使用 Hooks。
  4. 逻辑复用:
    • 类组件: 需要使用高阶组件 (HOCs) 或 render props。
    • 函数组件: 可以使用自定义 Hooks。
  5. 性能优化:
    • 类组件: 使用 PureComponent 或 shouldComponentUpdate 进行细粒度控制。
    • 函数组件: 可以利用 React.memo 和 useMemo。
  6. 未来发展趋势:
    • 类组件: 虽然 React 团队仍然支持,但鼓励新的开发使用函数组件和 Hooks。
    • 函数组件: 是 React 的未来方向,尤其是随着 Hooks 的引入。 结论: 类组件和函数组件都是 React 的核心部分,它们在渲染和传递 props 方面有很多相似之处。但随着 Hooks 的引入,函数组件变得更加强大和灵活,它们更简洁,易于读写,逻辑复用更直观,并且更符合 React 的未来发展方向。

42- setState 是同步的还是异步的?

在React中,状态更新(通过setState调用)可以是同步的也可以是异步的,这取决于被调用的上下文。 当setState被直接调用,例如作为事件处理器的一部分,例如绑定到按钮点击事件的函数中,React会将这些更新批处理(或“批量更新”),因为isBatchingUpdates标志被设置为true。这意味着React会积累所有的setState调用,并在事件处理结束后统一处理,以避免不必要的重渲染和性能问题。

但是,如果setState在一个异步函数中被调用,如在setTimeout、setInterval或者addEventListener的回调中,React就不会批处理这些更新,因为在这些情况下isBatchingUpdates标志通常是false。这意味着每次setState调用都会立即导致组件的重新渲染。

因此,当回答面试题时,您可以说明:setState通常在React事件处理器中是异步批处理的,以提高性能和减少不必要的渲染。但是,在诸如setTimeout、setInterval或DOM事件监听器中,setState可能会表现为同步的,除非使用了特定的技巧或者使用了React 18及以上版本的新特性。这个细微的差别是非常重要的,因为它影响到组件的渲染和性能优化。

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
	<script src="lib/react.development.17.js"></script>
	<script src="lib/react-dom.development.17.js"></script>
	<script src="lib/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
</body>
<script type="text/babel">
	class App extends React.Component{
		state = {
			count:100
		}
		render(){
			return (
				<div>
					<button onClick={()=>{
						// this.setState({
						// 	count:this.state.count+100
						// });
						// console.log(this.state.count);// 100


						setTimeout(()=>{
							this.setState({
								count:this.state.count+100
							});
							console.log(this.state.count);// 200
						})
					}}>{this.state.count}</button>
					<h3>标题</h3>
				</div>
			)
		}
		componentDidMount(){
			document.querySelector("h3").addEventListener("click",()=>{
				this.setState({
					count:9000
				});
				console.log(this.state.count);
			})
		}
	}
	// 第一个参数是渲染的内容,第二个参数是指定的根容器
	ReactDOM.render(<App/>,document.querySelector("#root"));
</script>
</html>

43- React 中的 ref 是什么?

在 React 中,ref是一个重要的属性,它允许我们直接访问 DOM 元素或组件实例。虽然 React 鼓励使用 props 和 state 来控制渲染和组件交互,但有时我们仍然需要直接操作 DOM,或者访问 React 组件实例上的某些方法。这时,ref就显得特别有用。

44- React 中的生命周期?

React 的组件生命周期可以被大致分为三个主要阶段:Mounting(挂载),Updating(更新)和 Unmounting(卸载)。

  1. Mounting(挂载)

    : 这些方法会在组件创建并插入到 DOM 中时被调用。

    • constructor: 组件的构造函数,最先被调用,用于初始化本地状态和绑定事件处理函数。
    • static getDerivedStateFromProps: 当 props 发生变化时,在 render 方法之前被调用。它应返回一个对象来更新状态或返回 null 表示不更新任何内容。
    • render: 唯一必需的方法。读取this.props和this.state并返回以下类型之一:React 元素,字符串和数字,fragments,Portals,布尔值或 null。
    • componentDidMount: 在组件输出被渲染到 DOM 之后立即调用。这是一个很好的地方去发起网络请求或设置订阅。
  2. Updating(更新)

    : 当组件的 props 或 state 改变时,会进入更新生命周期。

    • static getDerivedStateFromProps: 同上。
    • shouldComponentUpdate: 根据组件的 props 和 state 的变化,返回一个布尔值来决定 React 是否应继续渲染。默认返回true。这是一个优化性能的点,可以避免不必要的渲染。
    • render: 同上。
    • getSnapshotBeforeUpdate: 在最近的渲染输出提交到 DOM 之前被调用。它使组件能在可能的 DOM 改变之前从 DOM 捕获一些信息。
    • componentDidUpdate: 在更新发生后立即被调用。可以在此处进行 DOM 查询并触发网络请求。
  3. Unmounting(卸载): 当组件从 DOM 中移除时会调用。

    • componentWillUnmount: 在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,如无效的定时器,或取消网络请求,或清理任何在componentDidMount中创建的订阅。 此外,React 还引入了 Error Boundaries,用于捕获子组件树的 JS 错误,渲染备用 UI,而不是使整个组件树崩溃:
  4. static getDerivedStateFromError: 此生命周期被调用后,你可以渲染一个备用 UI。

  5. componentDidCatch: 此生命周期在报告错误日志时非常有用。 注意:

  6. React 16.3 开始引入了新的生命周期方法,并且弃用了componentWillMount, componentWillReceiveProps和componentWillUpdate。推荐使用新的生命周期方法。

  7. React 中新的生命周期:

    • getDerivedStateFromProps

      getDerivedStateFromProps 是 React 的生命周期方法,专为 class 组件设计。它的主要目的是让组件在接收新的 props(属性)时有机会产生新的 state(状态)。这是一个静态方法,这意味着它不能访问组件实例(即不能使用this关键字)。

      以下是 getDerivedStateFromProps 的一些关键特点:

      1. 调用时机:该方法在组件实例化后和每次组件接收新的 props 时都会被调用。
      2. 参数:它接受两个参数:nextProps 和 prevState。
        • nextProps:组件将要接收的新属性。
        • prevState:组件当前的状态。
      3. 返回值:它应该返回一个对象来更新状态,或者返回null来表示不需要更新任何状态。
      4. 无副作用:此方法只应用于返回一个 state 更新的对象或 null,而不应该有任何副作用(如发送 HTTP 请求或调用 setTimeout 等)。
      5. 静态方法:由于它是一个静态方法,所以不能访问 this。
      6. 使用场景:当组件的状态依赖于 props 的更改时,可以考虑使用此方法。但是,大多数情况下,最好使用其他方法,如 componentDidUpdate,或者完全避免将 props 映射到 state。
    • getSnapshotBeforeUpdate

      getSnapshotBeforeUpdate是一个生命周期方法,它允许您在 DOM 的实际更改之前捕获一些信息(例如滚动位置)。然后,该信息可以传递给componentDidUpdate方法,这样您就可以在更新之后使用这些信息。

      一般用途:当您的组件渲染的内容可能会改变滚动位置(例如,一个正在增长的列表),并且您想在 React 更新 DOM 后保持当前的滚动位置时,这个生命周期方法非常有用。

45- 什么是副作用(Side Effect)?

在 React 中,“副作用” 指的是组件渲染过程之外的操作—— 这些操作不直接参与 UI 渲染,但会影响组件或应用的状态。常见的副作用包括:

  • 数据请求(Ajax / fetch / axios)
  • DOM 操作(手动修改元素属性,如自动聚焦输入框)
  • 订阅/取消订阅(WebSocket、事件监听器)
  • 定时器(setInterval、setTimeout)

组件的主要任务是 “渲染 UI”(正作用),而副作用需要在渲染完成后执行,否则会阻塞渲染,影响页面性能。

46- 为什么 useState 返回的是数组而不是对象?

因为解构赋值的原因:

  • 返回数组,可以对数组中的变量命名,代码看起来也比较干净。
  • 返回对象,那就必须和返回的值同名,不能重复使用了。

47- React 与 Vue 比较有什么不同?

  • 组件化方式不同

    • React 组件包含状态和行为,所有组件共享一个状态树

    • Vue 每个组件都有自己的状态和行为,并且可以很容易将数据和行为绑定在一起

  • 数据驱动方式不同

    • React 单项数据流

    • Vue 双向数据绑定

  • 模板语法不同

    • React 模板语法是 JSX,all in js

    • Vue 模板语法是 Template、js、css,支持指令

  • 状态管理方式不同

    • React 状态管理:Redux、Mobx、zustand

    • Vue 状态管理:Vuex、Pinia

  • 性能优化方式不同

    • React 性能优化:React.memo、shouldComponentUpdate

    • Vue 性能优化:keep-alive、v-if

48- 什么是错误边界(Error boundary)

  • 部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

  • 当 React 检测到一个组件的渲染过程中抛出了错误,React 会通过错误边界机制捕获该错误。如果没有错误边界捕获该错误,React 将显示默认的错误页面并终止应用。

    错误边界的关键是它们实现了两个生命周期方法:static getDerivedStateFromError 和 componentDidCatch,这使得组件能够在出现错误时恢复正常。

  • 简单理解:用来捕获后代组件错误,渲染出备用页面。

  • 错误边界只能捕获渲染过程中、生命周期方法中的错误,不会捕获事件处理程序中的错误。要处理事件中的错误,可以使用 try…catch 语句。

  • 错误边界不会捕获异步代码中的错误。例如,setTimeout 或异步请求中的错误不会被错误边界捕获。为此,可以在异步操作中使用 try…catch 语句。

49.说一下 React 中的 Context?

React 的Context是一种传递数据的方法,允许数据能够被传递到组件树中的任何层级,而不必通过每一个层级的组件明确地传递。它被设计为解决当有许多层嵌套时,只是为了把数据传递给较低层级的组件而手动传递props的问题。

使用Context的步骤:

  1. 使用React.createContext创建一个新的上下文。
  2. 使用Context.Provider组件为子组件提供上下文值。
  3. 使用Context.Consumer组件或useContext hook 在任何子组件中访问上下文值。

50- 介绍一下useRef

useRef 是 React 提供的一个 Hook,它返回一个可变的 ref 对象,这个对象的 .current 属性可以被修改,并且它不会导致组件重新渲染。

下面是 useRef 的一些核心点:

  1. 创建 Ref: 当你调用 useRef(),它会返回一个像这样的对象: { current: initialValue }。你可以为它提供一个初始值,例如 useRef(0),但这并不是必需的。
  2. 持久性: 不像 useState,每次组件重新渲染时都会返回一个新的 state,useRef 会在整个组件生命周期中保持其对象不变。
  3. 使用场景:
    • 访问 DOM 元素: 可以使用 useRef 创建一个 ref,然后将它附加到 JSX 元素上,从而在组件内部访问该 DOM 元素。
    • 保留不触发渲染的可变数据: 如果你有一个值,你不希望它的变化导致组件重新渲染,可以将它存储在 ref 中。
    • 跟踪上一次的 props 或 state: 你可以使用 useRef 来跟踪前一个渲染周期中的值。

51- React Hooks的引入主要解决了哪些问题?

  1. 逻辑复用难度:类组件中的逻辑难以复用。虽然高阶组件(HOCs)和渲染道具(Render Props)提供了一定程度的复用,但它们往往导致代码结构复杂,产生所谓的”回调地狱”,难以理解和维护。
  2. 组件复杂性:在类组件中,复杂的业务逻辑通常散落在不同的生命周期方法中,这使得理解和维护变得困难。一个生命周期方法可能包含多个不相关的逻辑,这缺乏专注性且增加了混乱。
  3. this关键字的混淆:类组件中的this关键字经常导致混淆,特别是在事件处理和异步操作中。此外,类组件的方法需要正确绑定this,这增加了代码的复杂性。

52- 为什么hooks 需要遵循两条主要规则?

规则一:不要在循环语句、条件语句或嵌套函数内调用 hooks

规则二:只在 React 函数组或自定义hooks函数中调用 hooks

  • 不要在循环、条件语句或嵌套函数中调用 Hooks:这是为了保证 Hooks 每次渲染时都以相同的顺序调用,这对于 React 内部追踪 Hooks 状态非常重要。如果在条件语句或循环中使用 Hooks,可能导致每次组件渲染时 Hooks 调用的数量和顺序不一致,进而导致状态管理混乱。
  • 只在 React 函数组件或自定义Hooks函数中调用 Hooks:保证了 Hooks 只在 React 组件的上下文中被使用,这有助于 React 管理组件的生命周期和状态。在组件外部调用 Hooks 可能导致不可预测的行为,因为那些地方没有与 React 组件的生命周期和状态管理机制相结合。

53- useEffect和useLayoutEffect的相同点和不同点有哪些?

useEffect 和 useLayoutEffect 在 React Hooks 中都用于处理副作用,但它们有以下相同点和不同点:

相同点

  1. 功能:两者都用于在组件渲染后执行副作用操作。
  2. API:它们具有相同的 API 结构,即可以接收一个函数(副作用函数)和一个依赖数组。
  3. 清理机制:两者都允许返回一个清理函数,用于组件卸载时执行清理操作。

不同点

  1. 执行时机:
    • useEffect 在整个页面渲染和绘制完成后异步执行,不会阻塞浏览器的绘制过程。
    • useLayoutEffect 与 DOM 更新同步执行,会在浏览器绘制之前执行。因此,如果在 useLayoutEffect 中执行大量操作,可能会导致页面渲染的性能问题。
  2. 使用场景:
    • useEffect 适用于大多数副作用场景,如数据获取、订阅、以及在渲染后需要执行的操作。
    • useLayoutEffect 通常用于需要同步读取或更新 DOM 的场景,或者在 DOM 更新后立即需要执行的操作,例如测量布局。

简而言之,尽管两者在功能上相似,但 useLayoutEffect 的使用应更加谨慎,以避免因同步执行而引起的性能问题。大多数情况下,使用 useEffect 就足够了。

54- React.memo和React.useMemo有什么区别?

React.useMemo 和 React.memo 虽然都用于优化性能,但它们服务于不同的场景和目的,所以不能直接比较谁更好。它们的有效性取决于你正在处理的具体问题和使用场景。

React.useMemo

  • 目的:用于缓存复杂计算的结果。
  • 适用场景:当组件内部有昂贵的计算操作,这些操作仅依赖于特定的props或state,并且这些值不经常改变时,使用useMemo可以避免在每次渲染时重新计算。
  • 限制:它仅对组件内部的值或计算有效,不能防止组件本身的重新渲染。

React.memo

  • 目的:用于避免组件不必要的重新渲染。
  • 适用场景:当组件经常因父组件的更新而重新渲染,但其props实际上并没有发生变化时,使用React.memo可以防止这种不必要的渲染。
  • 限制:它适用于整个组件,而不是特定的值或计算。

55-说一下 React 中的 Context?

React 的Context是一种传递数据的方法,允许数据能够被传递到组件树中的任何层级,而不必通过每一个层级的组件明确地传递。它被设计为解决当有许多层嵌套时,只是为了把数据传递给较低层级的组件而手动传递props的问题。

使用Context的步骤:

  1. 使用React.createContext创建一个新的上下文。
  2. 使用Context.Provider组件为子组件提供上下文值。
  3. 使用Context.Consumer组件或useContext hook 在任何子组件中访问上下文值。

56- 说一下 React 如何避免不必要的渲染?

  • 类组件通过 React.PureComponent

PureComponent 是 React 提供的一个组件基类,它的核心特性是只有当它的 props 或 state 发生浅层变化时,它才会重新渲染。浅层比较会检查对象顶层的属性,而不是深度检查。如果对象的顶层属性没有变化,那么 PureComponent 就不会重新渲染,这可以提高性能。

使用 PureComponent 最适合于那些组件的 props 和 state 结构较简单,或者可以确保结构不会经常发生深度变化的场景。

  • 函数组件通过React.memo

React.memo是一个高阶组件,它可以用于优化那些仅仅依赖于其 props 变化的组件的重新渲染行为。换句话说,如果组件的 props 在连续的渲染之间没有发生变化,那么使用React.memo可以避免组件的不必要的重新渲染。

如果你有一个功能组件并且你想避免该组件因父组件的重新渲染而不必要地重新渲染,你可以通过React.memo包裹该组件:

工作原理

  1. 当一个组件被React.memo包裹时,React 会记住该组件上一次渲染的结果。
  2. 当这个组件的父组件再次渲染时,React 会使用浅比较(shallow comparison)来比较当前的 props 和上一次的 props。
  3. 如果 props 没有发生变化,React 会重用上一次的渲染结果,而不是重新渲染组件。
  4. 如果 props 发生了变化,组件将会被重新渲染。

57- React函数组件和类组件有什么相同点和不同点?

相同点

  1. 渲染 UI: 无论是函数组件还是类组件,都用于渲染 UI,并且可以接受 props 来定制显示内容。
  2. 使用方式和表达效果 函数组件和类组件的使用方式和表达效果是一样的

不同点

  1. 设计思想 类组件是面向对象编程,可以实现继承,函数组件是函数式编程
  2. 语法和结构: 类组件使用 ES6 类语法定义,拥有多个生命周期方法,而函数组件是普通的 JavaScript 函数,通常更简洁。
  3. Hooks使用: 只有函数组件可以使用 React Hooks(如 useState, useEffect),类组件不能使用这些钩子。
  4. 状态管理: 类组件通过 this.state 和 this.setState 管理状态,而函数组件可以通过 useState 钩子管理状态。
  5. this 关键字: 类组件中经常需要使用 this 来访问 props、状态和类方法。而函数组件中没有 this,一切都通过函数参数或钩子访问。
  6. 生命周期方法: 类组件有完整的生命周期方法(如 componentDidMount, componentDidUpdate, componentWillUnmount),而函数组件则使用钩子如 useEffect 来模拟这些生命周期。
  7. 组件重用和组合: 函数组件倾向于促进重用和组合的小型、专注的组件。而类组件更适合于需要复杂状态逻辑和生命周期管理的场景。
  8. Context 使用: 类组件通过static contextTypes使用Context,函数组件使用Consumer和useContext
  9. 性能优化: 函数组件使用React.memo、useMemo、useCallback进行性能优化,而类组件使用PureComponent和重写shouldComponentUpdate
  10. 逻辑复用: 函数组件使用自定义Hooks、高阶组件、组合、Render Props等方式进行性能优化,而类组件使用高阶组件(HOCs)、继承(Inheritance)、Mixins等方式进行逻辑复用
  11. 趋势 随着 React 的发展,函数组件因其简洁性和对钩子的支持而变得越来越流行,但类组件在某些复杂场景中仍有其独特的优势。

58- React中函数组件如何进行逻辑复用?

  • 自定义Hooks

React 的自定义 Hooks 是 React 功能的一个扩展,允许你在函数组件中重用状态逻辑,而无需改变组件结构。自定义 Hooks 本质上是 JavaScript 函数,但它们遵循一些特定的规则并利用 React 的基础 Hooks(如 useState, useEffect, useContext 等)。

以下是自定义 Hooks 的一些关键点:

  1. 重用状态逻辑:自定义 Hooks 允许你将组件逻辑提取到可重用的函数中。这意味着相同的状态管理和副作用逻辑可以在多个组件中共享,而无需重复代码。
  2. 命名约定:自定义 Hooks 应以 use 开头,这不仅是一种命名约定,也是 React 自动应用 Hooks 规则的一部分。
  3. 封装和组织:通过自定义 Hooks,你可以将相关逻辑组织在一起,使代码更易于理解和维护。例如,你可以创建一个 useForm Hook 来处理表单输入和验证。
  4. 使用基础 Hooks:自定义 Hooks 可以调用其他 Hooks,这意味着你可以在自定义 Hook 中使用 useState, useEffect, useContext 等基础 Hooks。
  • 高阶组件

高阶组件(HOCs)是 React 中用于重用组件逻辑的一种高级技术。简单来说,它们是参数为组件、返回值为新组件的函数。HOCs 让你可以把组件当作参数传入,然后返回一个新的“增强”组件,这个新组件拥有额外的属性或行为。

这里有几个关键点来理解高阶组件:

  1. 函数式编程概念:HOC 是一种基于函数式编程的模式。在函数式编程中,函数可以接受函数作为参数,并返回一个函数。
  2. 组件重用:HOC 允许你抽象出共通的逻辑,避免代码重复。例如,你可能有几个组件都需要进行数据获取,通过使用 HOC,你可以只编写一次数据获取逻辑,并将其应用于所有需要它的组件。
  3. 不修改原组件:HOC 不应该修改传入的组件,而是返回一个新的组件。这有助于保持原始组件的纯净性和可重用性。
  4. 组合:HOC 可以相互组合。你可以将一个组件通过多个 HOC 传递,每个都添加自己的功能。
  • Render Props

使用一个值为函数的prop(属性),这个函数返回要渲染的React元素。通过这种方式,可以在多个组件间共享逻辑。

59- 如何在ReactHooks中获取上一轮的值?

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";
function usePrevious(value) {
	const ref = useRef();
	useEffect(() => {
		ref.current = value;
	}, [value]);
	return ref.current;
}
function App() {
	const [value, setValue] = useState('初始值');
	const prevValue = usePrevious(value);
	return <div>
		<p>当前值: {value}</p>
		<p>上一轮的值: {prevValue}</p>
		<button onClick={() => setValue('新值')}>更新值</button>
	</div>;
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);

60- React Router如何在 React 应用中工作?

在 React 应用中,React Router 通过以下方式工作:

  1. 安装和设置:
    • 首先,通过 npm 或 yarn 安装 React Router 库。然后,在 React 应用的顶层组件中,使用<BrowserRouter>(或其他路由器类型)包裹应用的内容。
  2. 配置路由:
    • 使用<Routes>组件来定义应用的路由结构。在<Routes>内部,使用<Route>组件来定义每个路由的路径和对应的组件。
  3. 导航和链接:
    • 使用<Link>组件来创建导航链接,允许用户在应用中的不同页面间导航。
    • 使用useNavigate钩子在组件中编程式地导航。
  4. 参数和查询处理:
    • 使用useParams钩子来访问动态路由参数。
    • 使用useSearchParams钩子来处理 URL 查询参数。
  5. 嵌套路由:
    • 在 v6 中,可以更容易地创建嵌套路由。在父路由组件内部定义子路由,使得路由结构更加清晰和模块化。
  6. 重定向和守卫:
    • 使用<Navigate>组件来实现重定向。
    • 通过条件渲染和钩子来实现路由守卫,如保护私有路由。

React Router v6 的这些特性和工作方式使得在 React 应用中实现复杂的路由需求变得更加简单和高效。

61- 请解释 React Router 中BrowserRouter和HashRouter的区别?

在 React Router 中,<BrowserRouter>和<HashRouter>是两种不同的路由器组件,它们提供了不同的方式来处理 URL 和导航。理解这两者的区别对于选择适合特定应用的路由策略非常重要。

<BrowserRouter>

  1. 使用 HTML5 历史 API:
    • <BrowserRouter>使用 HTML5 的历史 API(特别是pushState、replaceState和popstate事件)来保持 UI 和 URL 的同步。
  2. 干净的 URL:
    • 这种方式产生的 URL 看起来很干净,没有额外的字符。例如:http://example.com/about。
  3. 服务器配置:
    • 使用<BrowserRouter>时,服务器必须正确配置,以便对所有可能的 URL 路径返回同一个 HTML 文件(通常是index.html)。这是因为应用的路由是前端控制的,服务器不会对每个路由有实际的页面。
  4. 适用场景:
    • <BrowserRouter>适用于大多数单页应用(SPA),特别是在你可以控制服务器行为时。

<HashRouter>

  1. 使用 URL 的哈希部分:
    • <HashRouter>使用 URL 的哈希部分(即#后面的部分)来保持 UI 和 URL 的同步。它不依赖于 HTML5 历史 API。
  2. 带哈希的 URL:
    • 这种方式产生的 URL 包含#,例如:http://example.com/#/about。哈希后面的部分表示应用的前端路由。
  3. 无需特殊服务器配置:
    • 由于 URL 的路径部分(#之前的部分)不变,服务器只需对根 URL 返回应用即可。这意味着不需要特殊配置,因为服务器不会看到哈希后的路径。
  4. 适用场景:
    • <HashRouter>适用于那些不能或不想在服务器上进行特殊配置的场景。它也是在文件系统(如file://)或无服务器环境中运行 React 应用的好选择。

总结

  • 选择<BrowserRouter>:如果你有权配置服务器,并且希望 URL 看起来干净、标准。
  • 选择<HashRouter>:如果你无法或不想配置服务器,或者在不支持 HTML5 历史 API 的环境中运行应用。

在实际应用中,选择哪种路由器通常取决于你对项目的部署环境和 URL 外观的需求。

62- 如何在 React Router 中配置路由?

在 React Router 中配置路由涉及几个关键步骤,包括设置路由器(如<BrowserRouter>或<HashRouter>),定义路由路径和关联的组件,以及在应用中使用链接进行导航。以下是配置路由的基本步骤:

1. 安装 React Router

首先,确保你已经安装了 React Router v6。如果没有,你可以通过 npm 或 yarn 来安装它:

npm install react-router-dom@6

或者

yarn add react-router-dom@6

从react-router-v7开始,将使用如下方式安装:

npm i react-router # 或 yarn add react-router

2. 设置路由器

在 React 应用的顶层组件中,使用<BrowserRouter>(或<HashRouter>,取决于你的需求)包裹应用的内容。这通常在index.js或App.js文件中完成:

import { BrowserRouter } from "react-router-dom";

function App() {
  return <BrowserRouter>{/* 应用的其余部分 */}</BrowserRouter>;
}

export default App;

3. 定义路由

使用<Routes>组件来定义应用的路由结构,并在其中使用<Route>组件来指定每个路由的路径和对应的组件。这通常在你的应用的主组件或专门的路由配置文件中完成:

import { Routes, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import NotFound from "./NotFound";

function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="about" element={<About />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

在这个例子中,Home组件将在访问根路径/时渲染,About组件将在访问/about时渲染,而NotFound组件将用于处理所有未匹配的路由(通常用于显示 404 页面)。

4. 使用链接进行导航

在应用的其他部分,使用<Link>组件来创建导航链接,允许用户在不同页面间进行导航:

import { Link } from "react-router-dom";

function Navbar() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </nav>
  );
}

5. 嵌套路由(可选)

如果你的应用需要嵌套路由,你可以在<Route>组件内部再定义<Route>组件:

<Route path="dashboard" element={<Dashboard />}>
  <Route path="profile" element={<Profile />} />
  <Route path="settings" element={<Settings />} />
</Route>

在这个例子中,Dashboard组件可以包含用于渲染Profile和Settings组件的<Outlet>组件,这些子路由将在访问/dashboard/profile和/dashboard/settings时渲染。

63- 请解释 React Router 中Link组件与普通a标签的区别

在 React Router 中,<Link>组件和传统的 HTML <a>标签都用于创建导航链接,但它们在处理页面导航时有重要的区别:

<Link>组件

  1. 单页应用(SPA)导航:
    • <Link>组件用于在 React 的单页应用中创建内部导航链接。当用户点击这些链接时,<Link>会告诉 React Router 更新 URL,但不会导致页面重新加载。
  2. 不触发全页刷新:
    • 使用<Link>进行导航时,页面不会重新加载。这意味着应用不会丢失当前状态,并且页面切换更快,因为只有必要的组件会被重新渲染。
  3. 与 React Router 集成:
    • <Link>组件是 React Router 的一部分,它知道如何与 React Router 的路由系统协同工作。它使用to属性来指定目标路由,例如 <Link to="/about">About</Link>。
  4. 自定义行为:
    • 你可以轻松地向<Link>添加自定义行为或样式,例如,根据当前路由路径改变链接的样式。

标签

  1. 传统的 HTML 链接:
    • <a>标签是 HTML 中的标准元素,用于创建指向不同页面的链接。在 React 应用中,它们通常用于指向应用外部的页面。
  2. 触发页面刷新:
    • 当使用<a>标签进行导航时,浏览器会进行全页刷新,加载新页面。这意味着应用的当前状态会丢失,且页面加载可能更慢。
  3. 不了解 React Router:
    • <a>标签不了解 React Router 的存在。它们只是简单地根据href属性的值改变浏览器的 URL,并加载新页面。
  4. 适用于外部链接:
    • 在 React 应用中,你通常会使用<a>标签来创建指向外部网站或页面的链接,例如 <a href="https://example.com">Visit Example.com</a>。

总结

  • 使用<Link>:当你需要在 React 的单页应用内部进行路由导航时,不希望页面重新加载。
  • 使用<a>:当你需要链接到应用外部的页面,或者需要页面完全重新加载时。

通过使用<Link>,React Router 能够优化导航体验,保持应用状态,并提供更快的页面切换,这对于提升用户体验至关重要。

64- 如何在 React Router 中传递和接收参数?

在 React Router v6 中传递和接收参数主要涉及使用 useParams 钩子和定义路由参数。下面是基本步骤:

定义带参数的路由

首先,在你的路由定义中,你需要指定参数。例如,如果你有一个用户详情页面,你可能会这样定义路由:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UserDetails from "./UserDetails";
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/user/:userId" element={<UserDetails />} />
        {/* 其他路由 */}
      </Routes>
    </Router>
  );
}

在这个例子中,:userId 是一个路由参数。

在组件中接收参数

在 UserDetails 组件中,你可以使用 useParams 钩子来访问这个参数:

import { useParams } from "react-router-dom";

function UserDetails() {
  let { userId } = useParams();

  // 使用 userId 来获取用户详情
  // ...

  return <div>{/* 渲染用户详情 */}</div>;
}

传递参数

当你想导航到这个路由并传递参数时,你可以使用 Link 组件或编程式导航:

import { Link } from "react-router-dom";

function UserList() {
  return (
    <div>
      {/* 假设 users 是一个用户数组 */}
      {users.map((user) => (
        <Link to={`/user/${user.id}`} key={user.id}>
          {user.name}
        </Link>
      ))}
    </div>
  );
}

或者使用编程式导航:

import { useNavigate } from "react-router-dom";

function SomeComponent() {
  let navigate = useNavigate();

  function goToUser(userId) {
    navigate(`/user/${userId}`);
  }
}

在 React Router 中传递查询参数(也称为搜索参数)也是一个重要的功能。这些参数通常用于在 URL 中传递非结构化数据,例如搜索词或分页信息。下面是如何传递和接收查询参数的步骤:

传递查询参数

查询参数可以通过 Link 组件或编程式导航添加到 URL 中。例如,假设你想根据搜索词过滤用户列表:

使用 Link 组件:

import { Link } from "react-router-dom";
// ‌encodeURIComponent‌ 是JavaScript中用于对URI组件进行编码的核心函数,会将特殊字符转换为十六进制转义序列,常用于URL参数编码以确保传输安全。
function SearchLink({ query }) {
  return <Link to={`/search?query=${encodeURIComponent(query)}`}>搜索</Link>;
}

使用编程式导航:

import { useNavigate } from "react-router-dom";

function SearchButton({ query }) {
  let navigate = useNavigate();

  function handleSearch() {
    navigate(`/search?query=${encodeURIComponent(query)}`);
  }

  return <button onClick={handleSearch}>搜索</button>;
}

在这些示例中,encodeURIComponent 函数用于确保查询字符串是正确编码的。

接收查询参数

在目标组件中,你可以使用 useSearchParams 钩子来访问查询参数。例如,如果你有一个用于显示搜索结果的组件:

import { useSearchParams } from "react-router-dom";

function SearchResults() {
  let [searchParams] = useSearchParams();
  let query = searchParams.get("query");

  // 使用 query 来获取和显示搜索结果
  // ...

  return <div>{/* 渲染搜索结果 */}</div>;
}

在这个例子中,useSearchParams 提供了一个接口来读取和修改当前位置的搜索参数。你可以使用 get 方法来检索特定的查询参数。

65- 如何在 React Router 中处理 404 或“未找到”页面?

在 React Router 中处理 404 或“未找到”页面的方法是使用一个没有 path 属性的 Route 组件,作为 Routes 组件的最后一个子元素。这个“无路径”路由将捕获所有未被前面的路由匹配的路径,从而充当一个通用的回退选项。以下是如何实现这一点的步骤:

步骤 1:创建一个 404 组件

首先,创建一个表示 404 页面的 React 组件:

function NotFound() {
  return (
    <div>
      <h2>404 Not Found</h2>
      <p>抱歉,您访问的页面不存在。</p>
    </div>
  );
}

步骤 2:在路由配置中添加 404 路由

然后,在你的路由配置中,添加一个不带 path 属性的 Route,并将其放在所有其他路由定义的最后:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import NotFound from "./NotFound";

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        {/* ...其他路由... */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
}

在这个例子中,<Route path="*" element={<NotFound />}> 是捕获所有未匹配路径的路由。当用户访问一个不存在的路由时,React Router 将渲染 NotFound 组件。

注意

  • 确保将 404 路由放在 Routes 组件的最后,以便只有在其他所有路由都不匹配时才触发。
  • 使用 path="*" 是 React Router v6 中推荐的方式,它表示匹配任何未被之前路由捕获的路径。

通过以上步骤,你可以在 React Router v6 中有效地处理 404 或“未找到”页面,为用户提供更好的导航和错误处理体验。

66- 请解释 React Router 中组件的用途及其如何使用。

在 React Router 中,<NavLink>组件是一种特殊类型的<Link>,用于创建导航菜单项。它的主要特点是可以根据当前的路由路径自动改变样式或类名,这对于指示当前激活的导航链接非常有用。

用途

<NavLink>的主要用途是在用户导航到与该链接相关的路由时,能够向用户反馈哪个链接是“活跃”的。这通常通过改变链接的样式或类名来实现,例如,改变字体颜色或背景色。

如何使用

使用<NavLink>时,你可以通过className或style属性来定义活跃和非活跃状态下的样式。React Router v6 提供了一个函数形式的className和style属性,这个函数接收一个状态对象,该对象包含isActive属性,表示链接是否活跃。

基本示例

import { NavLink } from "react-router-dom";

function Navigation() {
  return (
    <nav>
      <NavLink to="/" className={({ isActive }) => (isActive ? "active" : "")}>
        Home
      </NavLink>
      <NavLink
        to="/about"
        className={({ isActive }) => (isActive ? "active" : "")}
      >
        About
      </NavLink>
      {/* 其他链接 */}
    </nav>
  );
}

在这个示例中,当用户处于对应的路由时,对应的<NavLink>将获得'active'类名。

使用内联样式

你也可以使用内联样式来定义活跃状态:

<NavLink
  to="/profile"
  style={({ isActive }) =>
    isActive ? { color: "green", fontWeight: "bold" } : {}
  }
>
  Profile
</NavLink>

在这个例子中,当Profile链接活跃时,它的颜色会变为绿色,字体变为粗体。

总结

<NavLink>是 React Router 中用于创建导航链接的组件,它的特点是可以根据路由的活跃状态自动改变样式或类名。这使得它非常适合用于创建反映当前导航状态的菜单项。通过使用className或style属性的函数形式,你可以根据链接是否活跃来动态地应用不同的样式。

67- 在 React Router 中,useHistory、useLocation、useParams和useRouteMatch这些钩子的作用是什么?

在 React Router 中,useHistory, useLocation, useParams 是一组重要的钩子(Hooks),它们提供了访问和操作路由状态的能力。不过,值得注意的是,从 v6 开始,useHistory 被替换为了 useNavigate。我将逐一解释它们的作用:

useNavigate (替代了 useHistory)

  • 用途: useNavigate 钩子用于在应用中进行导航。它返回一个函数,该函数允许你编程式地改变当前的路由。

  • 示例:

    import { useNavigate } from "react-router-dom";
    
    function MyComponent() {
      let navigate = useNavigate();
    
      function handleClick() {
        navigate("/home"); // 导航到主页
      }
    
      return <button onClick={handleClick}>Go to Home</button>;
    }

useLocation

  • 用途: useLocation 钩子用于获取当前路由的位置信息。它返回一个 location 对象,该对象包含了当前 URL 的路径、搜索查询字符串、状态等信息。

  • 示例:

    import { useLocation } from "react-router-dom";
    
    function MyComponent() {
      let location = useLocation();
    
      // 使用 location 对象
      console.log(location.pathname); // 当前路径
      console.log(location.search); // 查询字符串
    
      return <div>Current path: {location.pathname}</div>;
    }

useParams

  • 用途: useParams 钩子用于访问当前路由路径中的参数。这对于动态路由(如 /user/:userId)非常有用。

  • 示例:

    import { useParams } from "react-router-dom";
    
    function UserDetails() {
      let { userId } = useParams(); // 假设路由是 /user/:userId
    
      // 使用 userId 参数
      return <div>User ID: {userId}</div>;
    }

总结

这些钩子是 React Router 的核心部分,它们提供了访问和操作路由状态的能力,使得在 React 应用中的路由管理变得更加灵活和方便。在 v6 中,通过使用这些钩子,你可以有效地管理路由跳转、获取 URL 参数和路径信息,从而为用户创造一个流畅的导航体验。

68- 如何在 React Router 中实现嵌套路由?

在 React Router 中实现嵌套路由是一个相对直观的过程。嵌套路由允许你在父路由组件内部定义子路由,这样可以更好地组织和管理复杂的路由结构。下面是实现嵌套路由的基本步骤:

1. 创建基本组件

首先,创建你的基本组件。例如,假设你有一个布局组件Layout,一个首页组件Home,和两个子页面组件About和Contact。

const Layout = ({ children }) => (
  <div>
    <header>Header</header>
    <main>{children}</main>
    <footer>Footer</footer>
  </div>
);

const Home = () => <div>Home Page</div>;
const About = () => <div>About Page</div>;
const Contact = () => <div>Contact Page</div>;

2. 设置路由

在你的应用中设置路由,使用<Routes>和<Route>组件来定义路由结构。在父路由组件内部定义子路由。

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="contact" element={<Contact />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

在这个例子中,Layout组件是顶层路由组件,它有三个子路由:Home(作为默认子路由,使用index属性),About和Contact。

3. 在父组件中渲染Outlet

在父组件(在这个例子中是Layout)中,使用<Outlet>组件来渲染子路由。<Outlet>组件是 React Router 提供的一个特殊组件,用于渲染当前路由匹配的子路由组件。

import { Outlet } from "react-router-dom";

const Layout = () => (
  <div>
    <header>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
      </nav>
    </header>
    <main>
      <Outlet /> {/* 子路由将在这里渲染 */}
    </main>
    <footer>Footer</footer>
  </div>
);

4. 使用链接导航

使用<Link>组件在应用中创建导航链接。这些链接将允许用户在不同的路由之间导航。

<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>

总结

通过以上步骤,你可以在 React Router 中实现嵌套路由。嵌套路由提供了一种强大的方式来组织和管理复杂的路由结构,使得代码更加模块化和易于维护。使用<Outlet>组件在父路由中渲染子路由是实现嵌套路由的关键。

69- 函数组件如何实现强制更新

在 React 函数组件中,没有像类组件那样的 forceUpdate() 方法,但我们可以通过一些技巧实现强制更新。

  • 封装成自定义 Hook,便于复用:

    function useForceUpdate() {
      const [, setState] = useState(0);
      return useCallback(() => setState(prev => prev + 1), []);
    }
    
    // 使用
    const forceUpdate = useForceUpdate();

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

01- 同一个页面三个组件请求同一个 API

面试要点/答案要点

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

示例代码(用 Promise 去重 + 简单缓存,适用于 fetch/axios)

// 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 取消与错误边界处理。”

02- 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 配置)

// 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 提供浏览器直接引入支持。”

03- 前端权限管理的模型

要点

  • 权限维度:认证(AuthN) vs 授权(AuthZ)。

  • 模型:

    • RBAC(基于角色 Role-Based)—— 角色映射权限集合(常见)。
    • ABAC(基于属性 Attribute-Based)—— 基于资源/用户/环境属性判断(更灵活)。
    • PBAC / ACL(基于策略或访问控制列表)。
  • 前端实践:最小权限展示、路由守卫、组件级权限指令、后端返回可授权菜单/按钮、策略中心化(策略服务)。

  • 防御性设计:前端只做 UI 隐藏,关键权限必须在后端校验。

代码示例:简单 RBAC(React + route guard)

// 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 实现细粒度策略。”

04- peerDependencies

要点

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

示例 package.json

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

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

05- pnpm 有什么优势

要点

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

示例:pnpm workspace 配置

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

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

06- eslint 作用

要点

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

示例:ESLint + Prettier 配置

// .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 能有效控制代码回退成本。”

07- browserslist

要点

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

示例:package.json 中配置

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

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


08- minify 代码压缩

要点

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

示例(webpack production 使用 terser)

// 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 以便排查。”

09- package-lock.json

要点

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

示例检查命令

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

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

10- serverless

要点

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

示例:简单 AWS Lambda handler(Node)

// 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)

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

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

11- 常见的 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 配置

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 处理编译与资源。”

12- 常见的 Plugin(见过哪些)

要点

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

示例(常见插件配置)

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、并行类型校验、代码压缩等。”

13- Loader 和 Plugin 的区别

要点

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

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

14- 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 产物。”

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

常用插件

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

示例:dev config

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 插件可以保持开发流畅且保证质量。”

16- 文件指纹是什么?怎么用?

要点

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

示例 webpack output

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

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

17- 如何优化 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

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 配置与并行化即可显著降构建时间。”

18- 写过 Loader 吗?缩写 loader 思路

要点

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

示例:一个简单的 loader(把 **// @upper** 标记的字符串转为大写)

// 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)进行复杂转换。”


19- 写过 Plugin 吗?编写 Plugin 思路

要点

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

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

// 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 钩子,能全局影响流程,适合跨模块或构建产物级别的操作。”

20- 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')**)

// 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'));
        }
      }
    }
  };
};

使用:

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

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

21- source map 是什么?生产环境怎么用?

要点

  • source map 映射编译/压缩后的代码到源代码,便于调试。

  • 生产使用策略:

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

示例(webpack)

// dev
devtool: 'eval-cheap-module-source-map'
// prod (不公开,但上传到错误收集)
devtool: 'hidden-source-map'

面试话术:“生产可使用 hidden-source-map 上传到错误监控(Sentry),避免直接暴露源码同时保留可追踪性。”

22- 文件监听原理

要点

  • 两种方式:轮询(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)

const chokidar = require('chokidar');
const watcher = chokidar.watch('./src', { ignored: /node_modules/ });
watcher.on('change', path => console.log('changed', path));

面试话术:“一般用 chokidar 做跨平台监听,优先使用文件事件,轮询仅作 fallback 或 CI 环境下使用。”

23- Webpack 热更新(HMR)原理

要点

  • HMR:在不刷新页面的情况下替换、添加或删除模块。
  • 流程:Dev server 通过 websocket 通知客户端有更新 -> 客户端获取更新模块 -> 运行模块的 accept 回调(module.hot.accept) -> 局部替换模块并执行更新逻辑 -> 若不能处理则触发全页 reload。
  • HMR 能保持应用状态(在组件层面实现),适合 UI 开发。

示例(React + webpack HMR)

// 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 回调,能保留应用内状态,提升开发效率。”

24- Webpack 事件机制了解吗?

要点

  • Webpack 使用 Tapable(类似事件/钩子系统),提供同步/异步钩子(tap/tapAsync/tapPromise)。
  • 常见钩子:compiler.hooks.beforeRun, compiler.hooks.emit, compilation.hooks.optimizeChunks 等。
  • Plugin 利用这些钩子改变构建流程或产物。

示例(插件内 tap)

compiler.hooks.emit.tapAsync('MyPlugin', (compilation, cb) => {
  // 修改 compilation.assets
  cb();
});

面试话术:“Webpack 钩子基于 Tapable,Plugin 通过 tap 注册不同阶段的逻辑。”

25- 了解 Webpack5 吗,相比 Webpack4 有哪些提升?

要点

  • 主要提升:

    • 更好的持久化 cache(filesystem cache)提升构建速度。
    • 内置 Module Federation(实验/插件生态),改善微前端。
    • 更现代的 asset modules(替代 file/url loaders)。
    • 更严格的默认性能/Tree shaking 改进。
    • 更小的 runtime 与优化机制。
  • 面试话术:“Webpack5 将性能与模块共享作为核心改进,尤其是持久化 cache 与 Module Federation 对大型项目受益显著。”

26- 模块联邦(Module Federation)理解

要点

  • Module Federation:Webpack5 特性,允许在运行时动态加载并共享多个独立构建的模块(micro-frontends)。
  • 优点:独立部署、共享 runtime/库,按需加载远程模块。
  • 注意事项:共享依赖版本协调、跨域/运行时加载策略、安全(验证远端模块来源)。

示例(host & remote 简要)

// 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 支持运行时共享模块,便于团队独立部署子应用并共享核心库。”

27- 什么是 Webpack?它的作用是什么?

要点

  • Webpack 是模块打包器(module bundler),从入口构建依赖图并输出静态资源(JS/CSS/图片)。
  • 作用:模块化、资源处理(loader)、构建优化(plugin)、代码分割、tree-shaking、生产构建。

面试话术:“Webpack 把前端项目的各种资源当模块来处理并生成优化后的静态资源,以支持现代前端开发流程。”


28- Webpack 中有哪些核心概念?

要点列表

  • Entry:应用入口
  • Output:构建输出
  • Module & Rules(Loader):模块及如何转换它
  • Plugins:扩展构建流程
  • Chunk:按需加载单元
  • Asset:静态资源
  • Dependency Graph:依赖图
  • Mode:production/development

面试话术:“理解 entry/module/plugin/chunk/output 这些基本概念能帮助掌握 webpack 的整体构建模型。”

29- Webpack 的 mode 是什么

要点

  • mode = 'development' | 'production' | 'none',配置默认优化项(如 uglify、tree shaking)。
  • development:更快构建、保留调试信息;production:启用优化、压缩、性能优化。

示例

module.exports = { mode: process.env.NODE_ENV || 'development' };

面试话术:“mode 决定内置优化集合,CI 中请显式指定 production 以获得体积与性能优化。”


30- 什么是代码分割(Code Splitting)?如何在 Webpack 中实现?

要点

  • 代码分割:将包拆成多个 bundle,按需加载(减少首屏体积)。

  • 实现方式:

    • 静态:entry 分割(multiple entries)
    • 动态:import()(动态 import)实现懒加载
    • CommonsChunk / SplitChunksPlugin(提取 vendor)
  • 场景:路由懒加载、按需组件、按功能拆包。

示例:React 路由懒加载 + webpack splitChunks

// 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 提取是代码分割的三大常见策略。”

各大互联网大厂性能优化相关面试真题&答案

01- 性能优化之项目初始化

说明 / 关键点

  • 项目初始化决定了运行时 & 打包基线:选择构建工具(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)

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):

npm i -D size-limit @size-limit/preset-small-lib

package.json 加入:

"size-limit": [
  {
    "path": "dist/index.js",
    "limit": "200 KB"
  }
]

02- 性能优化之辅助分析插件

说明 / 关键点

  • 分析工具用于定位体积热点与慢点: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:

npm i -D webpack-bundle-analyzer

webpack.config.js:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  // ...entry/output
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'report.html',
      openAnalyzer: false
    })
  ]
};

使用 source-map-explorer:

npm i -D source-map-explorer
# 构建之后:
npx source-map-explorer dist/assets/*.js

Chrome DevTools/Lighthouse:在真实设备/模拟慢网下跑 Lighthouse,并结合 Performance Trace 来找 First Contentful Paint / Long Tasks。

03- 性能优化之缩小范围(上)

说明 / 关键点

  • “缩小范围” 的上层策略是从宏观定位到微观:先用 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

// 简单 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));
    });
  });
}

后台只需简单接收并汇总慢页面。

04- 性能优化之缩小范围(中)

说明 / 关键点

  • 中层技术:用 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

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>
  );
}

05- 性能优化之缩小范围(下)

说明 / 关键点

  • 下层细化:拿到具体模块后,拆分检查依赖链(import graph)、重复依赖(多版本的 lodash)、第三方大包(charts、i18n、date libs)。
  • 常用工具:npm ls <pkg>、depcruise(dependency-cruiser)来检视依赖图,手动替换或将大库外部化(CDN 或动态 import)。

示例脚本:检查多版本

# 检查 lodash 多版本
npm ls lodash
# 或 pnpm why lodash
pnpm why lodash

替换示例

  • 将 moment 替换为 dayjs:
// before
import moment from 'moment';
console.log(moment().format());

// after
import dayjs from 'dayjs';
console.log(dayjs().format());

06- 性能优化之忽略模块

说明 / 关键点

  • 忽略(exclude/ignore)不需要打包的模块:例如测试工具、dev-only 模块、大型 polyfills(按需加载)或 server-only 代码。
  • Webpack: IgnorePlugin;Rollup / Vite: external config;也可以在 package.json 的 browser 字段或条件导出中排除平台不适用模块。

代码 demo(webpack IgnorePlugin)

const webpack = require('webpack');
module.exports = {
  plugins: [
    // 忽略 moment 的 locales(常见优化)
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/
    })
  ]
};

Vite / Rollup 外部化 vite.config.ts

export default defineConfig({
  build: {
    rollupOptions: {
      external: ['some-node-only-lib']
    }
  }
});

07- 性能优化之缓存与线程(上下)

说明 / 关键点

  • 缓存:浏览器端(长缓存策略 + 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)

// 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);
};

在主线程中使用:

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

export default defineConfig({
  cacheDir: 'node_modules/.vite_cache',
  build: {
    // 开启 esbuild 的 incremental/incremental 默认行为或使用 vite 内建缓存
  }
});

thread-loader(webpack)

module.exports = {
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          'thread-loader',
          'babel-loader'
        ],
        exclude: /node_modules/
      }
    ]
  }
}

08- 性能优化之压缩

说明 / 关键点

  • 代码压缩(JS/CSS):Terser/Esbuild(esbuild 更快但 terser 在某些场景有更细腻压缩选项)。开启 drop_console、pure_funcs。
  • 传输压缩:gzip / brotli(服务器在构建时预压缩静态资源可提升性能);HTTP/2+ 更有利于多个小文件传输。
  • 压缩图片:用 WebP、AVIF,并在构建或上传管道压缩(sharp、imagemin)。
  • 体积对比:构建后产生 .br / .gz 文件,配合 nginx 配置。

代码 demo — Vite 插件预压缩 & nginx 配置 安装预压缩插件:

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 文件)

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";
  }
}

09- 性能优化之样式

说明 / 关键点

  • 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)

module.exports = {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  // ...
}

抽取 Critical CSS(构建后用 critters 或 critical)

npm i -D critters-webpack-plugin

webpack.config.js:

const Critters = require('critters-webpack-plugin');
module.exports = {
  plugins: [
    new Critters({
      // inline: true,
      preload: 'swap'
    })
  ]
}

按路由加载样式(React + dynamic import)

const LazyPage = React.lazy(() => import('./page-with-big-css'));

该页面的 CSS 会随 chunk 被加载。


10- 性能优化之环境变量(上)

说明 / 关键点

  • 环境变量影响构建行为与打包结果(如是否启用 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

export default defineConfig(({ mode }) => ({
  define: {
    __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
    __APP_ENV__: JSON.stringify(process.env.APP_ENV || mode)
  }
}));

在代码中使用

if (process.env.NODE_ENV !== 'production') {
  console.log('debug only');
}
// 或者使用 define 常量
if (__APP_ENV__ === 'development') {
  // dev only
}

这样生产构建会把分支去掉,减少体积。

11- 性能优化之环境变量(下)

细节与实战:条件导入 / 条件打包

  • 利用条件编译(Define + dead code elimination)避免引入大型 dev-only lib(例如:只在 dev 引入 why-did-you-render)。
  • 使用 import() 在运行时按需加载环境相关模块。
  • 对第三方库采用 externals/CDN 在生产构建时替换。

代码 demo — dev-only 包

if (process.env.NODE_ENV === 'development') {
  // 这里使用 dynamic import,生产构建下该分支被去掉
  import('why-did-you-render').then(wdyr => {
    // init
  });
}

在 webpack/Vite 中开启 Terser/Esbuild 的 DCE 会消除掉这些分支。

12- 性能优化之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

{
  "sideEffects": [
    "*.css",
    "src/some-global-polyfill.js"
  ]
}

使用 lodash-es:

import { debounce } from 'lodash-es';

避免 import * as _ from 'lodash',会导致无法去掉没用的函数。

13- 性能优化之代码分割(上)

说明 / 关键点

  • 代码分割分为路由级分割、组件级分割、库分割(vendor)三类。
  • 动态 import(import() / React.lazy)是最常用方式;手动设置 manualChunks 进行长期缓存优化。
  • 把首次渲染所需的最少代码放在第一页,其它通过懒加载按需拉取。

示例(React + Vite)

// 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 手工分割:

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react')) return 'vendor_react';
            return 'vendor';
          }
        }
      }
    }
  }
});

14- 性能优化之代码分割(下)

更深入:预加载、预取、优先级控制

  • 使用 <link rel="prefetch"> 或 <link rel="preload"> 控制资源优先级。
  • React Router 等可以在空闲时间提前加载下一个页面的 chunk(link prefetch 或 requestIdleCallback)。
  • 注意:过度预加载会浪费带宽,应基于访问概率做智能预取。

代码 demo — 在路由改变前预fetch

// 简单的 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')));

15- 性能优化之提取公共组件(上)

说明 / 关键点

  • 公共组件提取分为代码级(common chunk / shared library)和组件库(monorepo 的 shared 包)。
  • 好处:减少重复代码、便于缓存和复用。缺点:过度共享会增加首屏体积(如果把不常用组件放公共 chunk)。
  • 方式:手动 manualChunks、使用 monorepo + pnpm workspace 抽出 @company/ui 包、或在运行时 CDN 加载公共 UI 库。

代码 demo — monorepo shared package(pnpm workspace) 仓库结构:

/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
}

组件示例:

// packages/ui/src/Button.tsx
import React from 'react';
export const Button = (props) => <button {...props} />;

主应用直接 import { Button } from '@company/ui',构建工具会把它作为单独包处理,利于复用与缓存。

16- 性能优化之提取公共组件(中)

策略 & 实践

  • 按需打包共享组件(shared chunk),把稳定且常用的组件放到 vendor/chunk,减少多次重复加载。
  • 做「按需导出」:不要把整个 component 库以一个巨型 bundle 导出,支持按文件导入(import { Button } from '@company/ui' 或 import Button from '@company/ui/Button')。
  • 在构建时利用 splitChunks(webpack)或 manualChunks(rollup/vite)把常用组件单独分离。

webpack splitChunks 示例

optimization: {
  splitChunks: {
    chunks: 'all',
      cacheGroups: {
      commons: {
        name: 'commons',
          test: /[\\/]src[\\/]components[\\/]/,
          minChunks: 2,
          priority: 10
      }
    }
  }
}

17- 性能优化之提取公共组件(下)

进阶:运行时共享(Module Federation / microfrontends)

  • 对于大型多团队项目,可使用 Module Federation(webpack 5)或微前端框架(single-spa/Wujie)实现运行时共享组件、按需加载并互相复用。
  • 好处:团队独立部署、减小单体包;坏处:复杂度高、版本兼容挑战。

Module Federation 简单示例(host/remote) remote webpack.config.js:

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:

new ModuleFederationPlugin({
  remotes: {
    remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js'
  },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
});

Host 可直接动态加载 remote 的 Button 组件。

各大互联网大厂Nodejs面试真题&答案

01- Node 是什么

答案: Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时,用于在服务端执行 JavaScript。它采用事件驱动、非阻塞 I/O 模型,非常适合高并发场景。

  • 特点:

    • 单线程 + 异步非阻塞
    • 事件驱动
    • 可用于构建网络应用、工具脚本等

代码 demo:

// hello.js
console.log('Hello Node.js');

// 运行方式
// node hello.js

02- Node 优缺点

答案:

  • 优点

    • 高性能:基于 V8 引擎
    • 异步非阻塞 I/O,适合高并发
    • JavaScript 全栈统一
    • 丰富的 npm 生态
  • 缺点

    • 单线程 CPU 密集型任务性能差
    • 异步编程复杂
    • 不适合计算密集型场景

代码示例:

// 异步示例
const fs = require('fs');
fs.readFile('./file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

03- Node 的应用场景

答案:

  • Web 服务器(Express、Koa)
  • 实时应用(聊天、游戏)
  • API 网关
  • 流处理(文件上传、音视频处理)
  • 脚本工具(自动化、爬虫)

代码 demo:

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello Node Server');
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000/');
});

04- Node 的 fs 模块

答案: fs 是 Node 内置模块,用于文件系统操作,包括读、写、删除、监控文件。

代码 demo:

const fs = require('fs');

// 读取文件
fs.readFile('./file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

05- Node 的 fs 文件基础知识

答案:

  • 文件路径:相对路径、绝对路径

  • 文件编码:utf8、binary

  • 同步/异步方法区别:

    • 异步:不阻塞主线程
    • 同步:阻塞主线程

代码 demo:

const fs = require('fs');
const path = require('path');

const filePath = path.join(__dirname, 'file.txt');

// 同步读取
const data = fs.readFileSync(filePath, 'utf8');
console.log(data);

06- Node 的 fs 文件方式 — 写入

答案:

  • fs.writeFile:覆盖写入
  • 异步、同步均可

代码 demo:

const fs = require('fs');

fs.writeFile('test.txt', 'Hello Node.js', 'utf8', (err) => {
  if (err) throw err;
  console.log('文件写入成功');
});

07- Node 的 fs 文件方法 — 追加写入

答案:

  • fs.appendFile:在文件末尾追加内容

代码 demo:

const fs = require('fs');

fs.appendFile('test.txt', '\n追加内容', (err) => {
  if (err) throw err;
  console.log('追加成功');
});

08- Node 的 fs 文件方法 — 文件拷贝

答案:

  • fs.copyFile 用于拷贝文件
  • 可指定覆盖模式

代码 demo:

const fs = require('fs');

fs.copyFile('test.txt', 'test_copy.txt', (err) => {
  if (err) throw err;
  console.log('文件复制成功');
});

09- Node 的 buffer 文件方法

答案:

  • Buffer 是 Node 用于处理二进制数据的全局对象
  • 文件读取时可以直接获取 buffer

代码 demo:

const fs = require('fs');

fs.readFile('test.txt', (err, data) => {
  if (err) throw err;
  console.log('Buffer:', data); // <Buffer 48 65 6c 6c 6f ...>
  console.log('String:', data.toString());
});

10- Node 的 buffer 使用方法

答案:

  • 创建 buffer:Buffer.alloc(size)、Buffer.from(string)

  • 常用方法:

    • toString()
    • slice()
    • length

代码 demo:

const buf1 = Buffer.alloc(10); // 10字节空buffer
const buf2 = Buffer.from('Hello Node'); // 从字符串创建

console.log(buf2.toString()); // Hello Node
console.log(buf2.length);     // 10

11- JWT 鉴权机制是什么?

答案:

  • JWT(JSON Web Token)是一种基于 JSON 的身份认证方案

  • 结构:header.payload.signature

  • 特点:

    • 无状态(服务端无需存储 session)
    • 可跨域
    • 自包含(payload 可存储用户信息)

示意:

header.payload.signature

12- JWT 鉴权机制如何实现?

答案:

  • 用户登录 -> 服务端生成 JWT -> 返回给客户端
  • 客户端每次请求携带 JWT
  • 服务端验证 JWT 是否有效

代码 demo:

const jwt = require('jsonwebtoken');
const secret = 'my_secret';

// 登录生成 token
const token = jwt.sign({ userId: 123 }, secret, { expiresIn: '1h' });
console.log('Token:', token);

13- JWT 鉴权机制 — 如何验证 token

答案:

  • 服务端使用 jwt.verify(token, secret) 验证
  • 验证成功即可继续访问,否则拒绝

代码 demo:

try {
  const payload = jwt.verify(token, secret);
  console.log('验证成功', payload);
} catch (err) {
  console.log('验证失败', err.message);
}

14- 分页功能实现

答案:

  • 常用思路:

    • limit + offset
    • 或使用 cursor 分页(大数据量)

代码 demo(假设数据库查询):

function getPageData(page = 1, pageSize = 10) {
  const offset = (page - 1) * pageSize;
  return db.query('SELECT * FROM users LIMIT ? OFFSET ?', [pageSize, offset]);
}

15- Node 中 Stream 的基本使用

答案:

  • Stream 是处理流式数据的接口,避免一次性读写大文件

  • 四种类型:

    • Readable
    • Writable
    • Duplex
    • Transform

代码 demo(读取大文件):

const fs = require('fs');
const readStream = fs.createReadStream('large.txt', 'utf8');

readStream.on('data', chunk => {
  console.log('读取数据块:', chunk.length);
});

readStream.on('end', () => {
  console.log('读取完成');
});

16- Node 中 Stream 的种类

答案:

  • Readable:可读
  • Writable:可写
  • Duplex:读写
  • Transform:可转换(例如 gzip 压缩)

代码 demo(可写 stream):

const fs = require('fs');
const writeStream = fs.createWriteStream('output.txt');
writeStream.write('Hello Stream\n');
writeStream.end();

17- Node 中 Stream 的使用场景

答案:

  • 大文件处理(上传/下载)
  • 数据管道(pipe)
  • 实时日志处理
  • 网络请求流

代码 demo(pipe):

const fs = require('fs');
const readStream = fs.createReadStream('large.txt');
const writeStream = fs.createWriteStream('copy.txt');

readStream.pipe(writeStream);

18- Node 的 process

答案:

  • process 是 Node 提供的全局对象,表示 Node 进程

  • 常用属性:

    • process.argv 命令行参数
    • process.env 环境变量
    • process.exit() 退出进程

代码 demo:

console.log('命令行参数:', process.argv);
console.log('环境变量:', process.env.NODE_ENV);
process.exit(0);

19- Node 中 process 的基本使用

答案:

  • 监听事件:

    • exit
    • uncaughtException
  • 设置环境变量

  • 获取内存使用情况

代码 demo:

process.on('exit', code => {
  console.log('进程退出码:', code);
});

console.log('当前内存使用:', process.memoryUsage());

20- Node 的 eventEmitter

答案:

  • EventEmitter 是 Node 事件驱动机制核心类
  • 可以注册、触发事件

代码 demo:

const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('sayHello', (name) => {
  console.log(`Hello, ${name}`);
});

emitter.emit('sayHello', 'Node');

21- Node 中 eventEmitter 的基本使用

答案:

  • 注册事件:on / once
  • 触发事件:emit
  • 移除事件:removeListener

代码 demo:

const EventEmitter = require('events');
const emitter = new EventEmitter();

function greet(name) { console.log('Hi', name); }
emitter.on('greet', greet);
emitter.emit('greet', 'Alice');
emitter.removeListener('greet', greet);

22- Node 中 eventEmitter 是如何实现的?

答案:

  • 内部维护一个事件名称到回调函数数组的映射
  • emit 遍历数组依次调用回调
  • 基于发布/订阅模式

简化实现 demo:

class MyEmitter {
  constructor() { this.events = {}; }
  on(event, fn) {
    this.events[event] = this.events[event] || [];
    this.events[event].push(fn);
  }
  emit(event, ...args) {
    (this.events[event] || []).forEach(fn => fn(...args));
  }
}

const emitter = new MyEmitter();
emitter.on('msg', (text) => console.log(text));
emitter.emit('msg', 'Hello!');

23- Node 的中间件

答案:

  • 中间件:函数链,接收 req, res, next
  • 在请求处理前/后做一些处理
  • 常见于 Express/Koa

代码 demo(Express):

const express = require('express');
const app = express();

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();
}

app.use(logger);

app.get('/', (req, res) => res.send('Hello Middleware'));
app.listen(3000);

24- Node 中间件的使用

答案:

  • 使用 app.use() 或路由级中间件
  • 可做鉴权、日志、异常处理等

代码 demo:

app.use((req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).send('Unauthorized');
  }
  next();
});

25- 在 Node 的中间件中如何添加日志模块?

答案:

  • 在中间件中使用 console.log 或专业日志库(winston, pino)
  • 可以记录请求时间、URL、状态码

代码 demo:

const fs = require('fs');
app.use((req, res, next) => {
  const log = `${new Date()} ${req.method} ${req.url}\n`;
  fs.appendFileSync('access.log', log);
  next();
});

26- Node 的事件循环机制是什么?

答案:

  • Node 单线程通过事件循环实现高并发
  • 异步任务分为宏任务(setTimeout, setImmediate)和微任务(Promise, process.nextTick)

27- Node 的事件循环流程

答案:

  1. 执行栈执行同步代码
  2. 执行微任务队列(process.nextTick, Promise.then)
  3. 执行 I/O 回调(Timer, IO callbacks)
  4. 执行 setImmediate
  5. 进入下一轮循环

代码 demo(展示执行顺序):

console.log('start');

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

Promise.resolve().then(() => console.log('promise'));

process.nextTick(() => console.log('nextTick'));

console.log('end');

输出顺序:

start
end
nextTick
promise
setTimeout
setImmediate

28- Node 的事件循环流程在面试中如何回答?

答案:

  • 简洁版:Node 单线程,事件循环 + 异步非阻塞 I/O,微任务优先于宏任务

  • 可画图解释执行栈、回调队列和事件循环阶段

  • 面试题重点:

    • 微任务 vs 宏任务
    • setImmediate vs setTimeout
    • process.nextTick

29- Node 的性能是什么?

答案:

  • 性能指标:

    • 响应时间
    • 吞吐量
    • CPU 占用
    • 内存使用
  • 性能瓶颈:

    • CPU 密集型任务
    • 大量同步 I/O
    • 阻塞事件循环

30- Node 的监控

答案:

  • 常用指标:

    • Event Loop 延迟
    • CPU/内存
    • 请求数、错误率
  • 工具:

    • pm2
    • node-clinic
    • prometheus + grafana
    • 内置 process API

代码 demo(监控 event loop 延迟):

const { performance } = require('perf_hooks');

setInterval(() => {
  const start = performance.now();
  setImmediate(() => {
    const delay = performance.now() - start;
    console.log('Event loop delay:', delay.toFixed(2), 'ms');
  });
}, 1000);

31- Node 的性能优化

答案:

  • 异步 I/O,避免阻塞
  • 使用 Stream 处理大文件
  • 利用 cluster 或 worker_threads 多线程处理 CPU 密集任务
  • 缓存热点数据
  • 减少中间件和同步阻塞

代码 demo(cluster 多核):

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) cluster.fork();
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} exited`);
    cluster.fork();
  });
} else {
  http.createServer((req, res) => {
    res.end('Hello Cluster');
  }).listen(3000);
}

各大互联网大厂小程序相关面试真题&答案

1- 微信小程序的实现原理

要点(概念层面)

  • 小程序是一个运行在微信容器(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(支付、定位等)

代码演示(最小运行示例) 项目结构:

/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; }

2- 微信小程序的支付流程

要点(高层流程)

  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)

这是最简示例,真实项目要做签名、错误处理、幂等、证书、回调验证等更严谨的实现。

// 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

// 调用后端创建订单并拉起支付
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);
    }
  }
});

3- 提高微信小程序应用速度的手段(性能优化)

按“启动/渲染/网络/运行时”分类列出常用手段,并给代码/配置示例。

启动/包体与分包

  • 主包最小化:把非首屏资源放到分包、子包、异步组件里。

    • app.json 中配置 subPackages。
{
  "subPackages": [
    {
      "root": "packageA",
      "pages": ["pages/a/index"]
    }
  ]
}
  • 使用插件 & 云函数按需加载:将耗时逻辑放到后端/云函数。

渲染优化

  • 尽量减少 setData 的数据量与频繁调用:只更新必要字段,批量更新。
// 不建议:每项单独 setData
this.setData({ a: 1 });
this.setData({ b: 2 });
// 建议:
this.setData({ a: 1, b: 2 });
  • 组件化/纯数据组件:避免把大量无关数据放在父组件导致不必要重渲染。
  • WXS 用于小量计算/模板逻辑:在视图层做少量处理,减少逻辑层通信。
// utils.wxs
module.exports = {
  format: function(s) { return s + '!' }
}

网络/请求

  • 开启 gzip,使用 CDN:后端返回静态资源使用 CDN;接口响应体支持 gzip。
  • 缓存策略:使用 wx.setStorageSync 缓存列表/静态数据,利用 ETag/If-Modified-Since。
  • 合并请求与接口分页:接口聚合或多路合并减少网络多次往返。

列表渲染

  • 虚拟列表(长列表不可一次渲染):用第三方或手写虚拟滚动(只渲染可视区)。
  • 分段渲染:首屏先渲染必要项,非关键延后。

主线程/计算密集

  • 使用 Worker(小程序支持 Worker):把 hash、图片压缩等放到 Worker,避免卡 UI。
// 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

{
  "pages": ["pages/index/index"],
    "subPackages": [
    { "root": "packageLarge", "pages": ["pages/large/index"] }
  ]
}

在首页点击跳转时再加载:

wx.navigateTo({ url: '/packageLarge/pages/large/index' });

4- 微信小程序中路由跳转方式与区别

微信提供 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 流程。

示例代码

// 从首页 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 });

5- 微信小程序的登录流程

要点(标准流程)

  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)

小程序端:

// 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):

// 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;

6- 微信小程序的生命周期函数有哪些

分两类: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 等)。

示例

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'); }
});

7- 你对微信小程序的理解?优缺点?

理解(一句话): 微信小程序是“在微信生态内可快速触达、低门槛发布、受限容器环境下运行”的轻量级应用形态,适合轻量业务、入口导流、工具型场景。

优点

  • 低门槛分发:用户无需安装即可使用,强大的流量入口(公众号、社群、扫一扫、分享等)。
  • 统一平台 SDK:微信提供支付、授权、位置、开放能力、模板消息等一站式能力。
  • 快速迭代:发布流程简单,上线速度快。
  • 跨平台(近):在微信生态内能覆盖 iOS/Android,同时对开发者门槛友好。

缺点 / 局限

  • 受限的能力:沙箱限制、API 受限(如复杂 native 能力弱),不能随意访问设备底层。
  • 包体与性能限制:首包体积限制、页面栈限制(10 层),渲染能力不及原生应用,在复杂可视化、高性能场景下受限。
  • 平台依赖:依赖微信生态,商业规则与审核可能限制某些功能。
  • 调试与兼容性:微信版本差异、真机差异、不同手机厂商表现差异等。

面试展示角度(建议回答模板)

  • 说明应用场景(适合电商轻量成交页、工具、活动页、SaaS 轻量客户端等),并给出优缺点,最后给出一个 mitigation 策略,例如在高性能场景用原生小程序或 H5 + 原生容器,或把复杂计算放服务端/Worker。

代码合集(附更多实用示例)

A. setData 性能优化示例(批量更新/最小化更新)

// 误用:每次都 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)

{
  "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)

// 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 与切片元数据)

// 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(前端主逻辑)

// 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)

// /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,整页分片)

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、定时抓取。

04- 场景面试 — 移动端适配

目标:从 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(适配)

/* 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();

05- 场景题 — 如何修改第三方 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(推荐面试答案)

# 安装 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 指向该私服版本。

06- 场景题 — 一个链接两应用(同一链接/按钮在两个不同应用中打开/路由)

场景解释:单一 URL 同时被多个子应用/入口使用(微前端场景),或一个链接在不同应用中打开(主应用/子应用共享跳转)。

解决思路

  • 统一路由策略(例如:hash / path)并通过 query 参数或 fragment 指明目标 app。
  • 主入口处理:当链接进入主应用,根据 path 或参数决定 mount 哪个子应用(微前端框架如 qiankun/Wujie/micro-app)。
  • 如果需要在两个应用中同时打开(一个新窗口打开另一个),使用 target + window.open with URL + params。

示例(微前端 qiankun 路由策略)

// 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')
  },
  // ...
]);

07- 场景题 — 实现网页加载进度条

目标:页面加载时(首屏/路由切换/资源加载)展示进度条(类似 NProgress),并尽量模拟真实进度。

实现要点

  • 对静态资源:使用 window.fetch / XHR 拦截或 Resource Timing API 来估计进度(有限)。
  • 对路由切换:在路由开始时 start(), 在请求成功/页面渲染完成时 done()。
  • 模拟进度策略:快速到 80%-95%,等待真实事件完成再到 100%,并加上缓慢推进(decay)避免瞬间跳满。
  • 使用 CSS 动画 + requestAnimationFrame 做平滑。

简单实现(NProgress 风格)

// 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>

08- 场景题 — 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

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);
  // 进入降级方案:隐藏相关功能、显示提示等
});

09- 场景面试 — 前端水印功能(图片/页面/导出到 PDF 的水印)

目标:实现抗篡改、可配置的文本/图片水印,能在导出/截图中保留。

思路

  • 页面水印:使用 fixed/flexible 的重复背景层(CSS + Canvas 生成图片,设置 pointer-events:none)。
  • 防移除:用多层水印或定期校正(定时检查 DOM)。
  • 导出(PDF/截图):在导出前把水印绘制到 canvas/PDF。

示例(Canvas 生成图像作为背景)

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);
}

10- 场景面试 — 大数运算(JS 精度问题、实现大整数/高精度浮点)

目标:处理精度丢失(JS Number)场景,支持加减乘除/四舍五入/大整数、任意精度小数。

做法

  • 使用 BigInt(原生)处理整数(但不支持小数)。
  • 使用高精度库 decimal.js、big.js、bignumber.js 来处理小数场景。
  • 或者实现字符串算术(手写大数加减乘),适合面试考题实现。

简单字符串大整数加法(面试算法题)

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('');
}

11- 场景面试 — 设计全站请求耗时统计工具(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)

// 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 分组,做告警。

12- 场景面试 — 函数式编程(FP 在前端的应用)

目标:解释函数式编程概念并举例(纯函数、不可变数据、高阶函数、组合、柯里化、侧边效果最小化)。

面试要点与示例

  • 纯函数:相同输入返回相同输出,无副作用。
  • 不可变:使用 Object.freeze / 结构性共享(immer)
  • 高阶函数:map, filter, reduce 或自定义 compose
  • 柯里化/组合:提高可复用性
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)

13- 场景面试 — 电影票选座功能(座位布局、并发抢占、锁座、支付完成释放)

目标:设计票选座功能,解决并发冲突、锁定、超时释放、支付一致性。

关键设计

  • 座位模型: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 分布式锁;使用消息队列确保幂等

前端交互(示例)

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 }
}

后端示例(伪代码)

-- 悲观 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:

SETNX seat:{id} {lockInfo} EX 300

14- 场景面试 — 图片性能优化方案

目标:降低页面图片的加载成本和渲染成本,提高 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)

<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>

15- 场景题 — 移动端上拉加载 / 下拉刷新

目标:实现移动端上拉加载更多与下拉刷新体验(带阻尼效果、节流、避免冲突)。

要点

  • 用 touch 事件或 pointer events 实现下拉;要兼容浏览器默认下拉(在 WebView 中常禁用默认)。
  • 下拉刷新:拖拽距离阈值(e.g. 60px)+ 回弹动画 + 状态机(idle -> pulling -> ready -> refreshing -> done)。
  • 上拉加载:监听滚动到接近底部 scroll 或 IntersectionObserver(更优)。
  • 避免冲突:在下拉时禁用滚动或在顶部检测才启下拉。

简单实现(下拉刷新)

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 事件)。

16- 场景面试 — 深度 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

<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>

17- 无感刷新Token:如何做到让用户“永不掉线”

没有什么比在用户操作得正嗨时,突然提示“登录已过期,请重新登录”的提示更让人沮丧的了。这种突兀的中断不仅破坏了用户体验,甚至可能导致未保存的数据丢失。

然而,我们都知道,出于安全考虑,用于身份验证的 Token(通常是 Access Token)必须有较短的有效期。那么,我们如何在保证安全的前提下,创造一种“永不掉线”的丝滑体验呢?

问题的根源:Access Token 的“天生矛盾”

首先,我们要理解为什么需要刷新 Token。

我们通常使用 Access Token 来验证用户的每一次 API 请求。为了安全,Access Token 的生命周期被设计得很短(例如 30 分钟或 1 小时)。如果有效期太长,一旦泄露,攻击者就能在很长一段时间内冒充用户进行操作,风险极高。

这就产生了一个矛盾:

  • 安全性要求:Access Token 有效期要短。
  • 用户体验要求:用户不想频繁地被强制重新登录。

为了解决这个矛盾,Refresh Token 应运而生。

核心理念:双 Token 认证系统

无感刷新机制的核心在于引入了两种类型的 Token:

  1. Access Token(访问令牌)

    • 用途:用于访问受保护的 API 资源,附加在每个请求的 Header 中。
    • 特点:生命周期短(如 1 小时),无状态,服务器无需存储。
    • 存储:通常存储在客户端内存中(如 Vuex/Redux),因为需要频繁读取。
  2. Refresh Token(刷新令牌)

    • 用途:当 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. 处理刷新结果:

    • 刷新成功:服务器验证 Refresh Token 有效,生成一个新的 Access Token(有时也会返回一个新的 Refresh Token,这被称为“刷新令牌旋转”策略,可以提高安全性),并将其返回给客户端。
    • 刷新失败:如果 Refresh Token 也过期了或无效,服务器会返回错误(如 403 Forbidden)。这意味着用户的登录会话彻底结束。
  7. 重试与终结:

    • 若刷新成功:客户端用新的 Access Token 自动重发刚才失败的那个 API 请求。用户完全感觉不到任何中断,数据操作无缝衔接。
    • 若刷新失败:客户端清除所有认证信息,强制用户登出,并重定向到登录页面。

实战演练:使用 Axios 拦截器实现无感刷新

Axios 的拦截器是实现这一流程的完美工具。下面是一个完整且考虑了并发问题的实现方案。

1. 创建 Axios 实例

首先,我们创建一个单独的 Axios 实例,方便统一管理。

// 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. 核心:响应拦截器

这是实现无感刷新的关键。

// 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 应用提升用户体验的“标配”。它将身份验证的复杂性隐藏在后台,为用户提供了一个流畅、不间断的操作环境。

实现这一机制,不仅仅是写几行代码,更是对认证流程、安全性和用户体验三者之间平衡的深刻理解。