本轮要点: react hooks、列表和key、高阶组件、性能优化、react 渲染机制、React函数组件更新、react 组件设计模式、全方位解读this、类型判断
本轮共 14 道题。答案默认折叠,便于先自行作答。
1. 项目中遇到的最大技术挑战是什么
题目要点
略
参考答案
略
2. 项目整体架构是什么样的
题目要点
略
参考答案
略
3. 在项目中,你们是如何进行前端代码的模块化和组件化的
题目要点
- 模块化:代码拆分成独立功能模块,使用 ES Module 标准
- 组件化:UI拆分成可复用组件,封装结构、样式和逻辑
- 结合构建工具支持模块管理和代码分割
- 组件通信通过 props、事件、状态管理实现
- 合理设计文件结构和规范,提升维护性和扩展性
- 注意职责单一、样式隔离和性能优化
- 防止模块和组件划分不合理导致管理混乱
参考答案
考察点
- 理解模块化和组件化的概念及其在前端开发中的重要性
- 熟悉常见的模块化规范(如 CommonJS、ES Module)及其区别
- 掌握组件化设计思想和实现方式,尤其是在主流框架中的实践
- 能结合项目需求合理组织代码结构,提高复用性和维护性
参考答案
一、原理说明
- 模块化 指将代码拆分成独立的、功能明确的模块,每个模块有自己的作用域和接口,避免全局污染,提高代码可维护性。
- 常见规范包括:
- CommonJS(Node.js环境,使用
require和module.exports) - AMD(异步模块定义,适用于浏览器异步加载)
- ES Module(ESM)(现代标准,使用
import和export,支持静态分析)
- CommonJS(Node.js环境,使用
- 常见规范包括:
- 组件化 是模块化的进一步细化,专注于 UI 层面,将界面拆分成独立且可复用的组件,封装逻辑、样式和结构。
- 组件通常包含状态管理、事件处理、生命周期管理等,方便组合和维护。
- 主流框架如 Vue、React、Angular 都鼓励基于组件开发。
- 组件通常包含状态管理、事件处理、生命周期管理等,方便组合和维护。
二、项目中模块化和组件化的实践
1. 模块化管理
- 项目采用 ES Module 作为标准模块规范,利用
import和export进行模块间依赖管理。 - 通过合理拆分功能模块(如工具库、接口服务、状态管理、业务逻辑)形成清晰的依赖关系,便于维护和测试。
- 使用构建工具(Webpack、Vite)支持模块打包和代码分割,提高加载效率。
- 配合
alias和路径管理,保持模块引用简洁,提升开发体验。
2. 组件化设计
- 界面层面,拆分成粒度合适的可复用组件,满足单一职责原则。
- 采用状态提升、属性传递和事件回调机制实现组件间通信。
- 使用组件库(如 Element UI、Ant Design)或自研组件库提升开发效率。
- 结合样式模块化(CSS Modules、Scoped CSS、CSS-in-JS)实现样式隔离,防止样式冲突。
- 组件支持配置化和插槽机制,提高灵活性和扩展性。
3. 代码组织结构
- 按业务域或功能划分文件夹,模块和组件分层清晰,便于定位和扩展。
- 统一入口导出,方便模块聚合和复用。
- 编写完善的文档和规范,确保团队成员对模块和组件使用方式达成共识。
4. 优势与价值
- 提高代码复用率,减少重复开发和维护成本。
- 降低耦合度,提升系统可扩展性。
- 加快开发节奏,方便多人协作。
- 利用现代构建工具实现性能优化。
三、常见误区与注意点
- 模块划分过细或过粗 导致管理困难或代码臃肿,需根据项目复杂度合理拆分。
- 组件职责不明确,混合业务逻辑和表现层,影响复用和测试。
- 忽视样式隔离,导致样式污染和冲突。
- 组件间通信设计不合理,导致状态管理混乱。
- 忽略模块和组件的性能优化,如避免无意义的重新渲染和重复加载。
4. 项目中做过哪些性能优化方面的工作
题目要点
- 资源按需加载与代码拆分减少首屏加载时间
- 静态资源压缩、合并和 CDN 加速
- 图片优化与懒加载
- 减少 DOM 操作,合理使用虚拟 DOM
- JS 任务拆分,使用防抖节流和 Web Worker
- 接口合并、缓存和 HTTP/2 多路复用
- 持续性能监控与数据驱动优化
- 避免过度拆分和缓存策略失误
- 注重整体性能和用户设备多样性
参考答案
考察点
- 理解前端性能优化的核心目标与常见瓶颈
- 掌握多种性能优化手段及其适用场景
- 能结合具体项目场景,分析问题并提出有效优化策略
- 熟悉常用性能监控和评估工具,验证优化效果
参考答案
一、原理说明
- 前端性能优化的核心目标是提升页面加载速度、响应速度和交互流畅度,从而改善用户体验,降低用户流失。
- 性能瓶颈通常来源于资源加载慢、渲染阻塞、JavaScript 执行耗时、内存泄漏和网络请求频繁等。
- 现代性能优化涉及多个层面,包括网络层、资源层、渲染层和代码执行层。
- 性能优化不仅是技术问题,更涉及合理架构设计和业务合理拆分。
二、项目中的具体优化实践
1. 资源加载优化
- 采用代码拆分(Code Splitting)和按需加载,避免一次性加载大量无关代码,缩短首屏加载时间。
- 使用 CDN 分发静态资源,提升资源下载速度。
- 资源压缩和合并(如压缩 JS/CSS,合并小文件减少请求数量)。
- 图片优化(WebP 格式替代、图片懒加载、合理尺寸裁剪)。
- 利用浏览器缓存策略(Cache-Control、Service Worker)减少重复请求。
2. 渲染性能优化
- 减少 DOM 操作次数,合并多次修改,避免触发频繁回流和重绘。
- 使用虚拟 DOM 和虚拟列表技术优化大量数据渲染。
- CSS 动画尽量使用 GPU 加速属性(如 transform、opacity)。
- 避免长任务阻塞主线程,合理拆分 JS 任务,防止界面卡顿。
3. JavaScript 执行优化
- 减少不必要的计算和事件绑定,防抖节流常用来优化频繁触发的事件。
- 使用 Web Worker 将计算密集型任务移至后台线程,避免主线程阻塞。
- 利用 Tree Shaking 去除未使用代码,减小包体积。
- 预编译和缓存机制提升运行效率。
4. 网络请求优化
- 接口合并,减少请求次数。
- 接口缓存,避免重复请求同一数据。
- 接口压缩与批量请求。
- 使用 HTTP/2 多路复用,提升请求并发性能。
5. 性能监控与持续优化
- 集成性能监控工具(如 Lighthouse、Chrome DevTools Performance、RUM 埋点等)。
- 通过埋点监控首屏时间、接口响应时间、动画帧率等关键指标。
- 根据监控数据持续跟踪性能瓶颈,迭代优化。
三、常见误区与注意点
- 只注重单一优化手段,忽视整体性能提升策略。
- 盲目过度拆分导致请求过多,反而影响性能。
- 缓存设置不合理,导致用户看到过期内容。
- 不关注性能监控数据,缺乏闭环改进。
- 忽视用户设备差异,优化方案不够适配低端设备。
5. 在移动端开发中,你们是如何处理不同设备的适配问题的
题目要点
- 了解并使用 viewport 设置布局基础
- 灵活组合 rem/vw/flex/media query 等单位和布局方案
- 使用 postcss 工具链统一适配流程
- 考虑高DPR设备图像清晰度适配
- 针对横屏、Pad 等极端设备考虑额外样式方案
- 注重兼容性测试,处理浏览器差异
参考答案
考察点
- 是否了解常见的移动端适配方案及原理
- 是否能结合业务场景选择合理的适配策略
- 是否关注兼容性、用户体验和性能平衡
- 是否能系统性总结适配技术手段的优缺点
参考答案
一、适配问题的来源
移动端设备种类繁多,存在如下适配挑战:
- 屏幕尺寸不同(如 iPhone SE 到 iPad Pro)
- 分辨率与像素密度不同(如1x 到 4x)
- 不同平台(iOS、Android)行为差异
- 浏览器内核差异(微信内嵌、Safari、Chrome 等)
二、常见适配方案及实践
1. 使用视口 meta 标签设置
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
- 保证页面以设备宽度为基准渲染,避免缩放失真
- 是一切移动端适配的基础
2. flex 弹性布局 + 百分比布局
- 使用
flex实现自适应布局、对齐方式 - 使用
%控制元素宽高随父元素变化自动适应
3. rem 适配(结合 lib-flexible 或 postcss-pxtorem)
- 基于
html { font-size }动态设置根字体大小 - 所有长度用
rem表示,实现随屏幕宽度缩放
示例代码:
document.documentElement.style.fontSize = document.documentElement.clientWidth / 10 + 'px';
- 优点:适配思路清晰,开发体验好
- 缺点:对第三方组件兼容性较差,需要统一处理单位转换
4. vw/vh 适配
- 使用视口单位(vw = 1%视口宽度,vh = 1%视口高度)作为长度单位
- 优点:原生CSS支持,无需额外JS计算
- 缺点:iOS下 Safari 某些版本 vh 不稳定;不利于对字体的精确控制
5. 媒体查询 Media Query
@media screen and (max-width: 768px) {
/* 针对窄屏设备样式 */
}
- 适合做断点响应式布局(如 PC/H5 两端共用一套代码)
- 可配合 CSS Modules 或 Sass 编写模块化响应样式
6. 多套 UI 组件或主题切换
- 对于分辨率特别极端或平台风格差异大的情况(如横屏、Pad、TV)
- 采用不同主题、样式、交互进行适配处理
三、其他补充策略
- 高清方案:针对高DPR(devicePixelRatio)设备使用多倍图或 SVG 图形
- viewport-fit=cover:适配 iPhone X 等刘海屏安全区域
- 字体大小限制:Android 浏览器允许用户放大字体,需设置
user-scalable=no或动态缩放限制
6. 闭包的定义和应用场景是什么
题目要点
- 闭包是在函数中返回函数,并访问外部变量形成的作用域链
- 常用于封装私有变量、延长变量生命周期、记忆函数等
- 项目中常见于定时器、回调函数、高阶函数中
- 滥用闭包会导致内存泄漏或性能下降,需注意释放引用
参考答案
考察点
- 是否理解闭包的定义与形成机制
- 是否掌握闭包的典型应用场景和实际项目中的使用方式
- 能否识别闭包带来的性能和内存问题
参考答案
一、闭包的定义
闭包(Closure)是指:
一个函数能够“记住”并访问其定义时的词法作用域,即使这个函数在其词法作用域之外执行。
换句话说,当一个函数内部返回另一个函数,并且这个内部函数访问了外部函数的变量,那么这个内部函数就形成了闭包。
形成条件:
- 函数嵌套(函数内部返回或引用另一个函数)
- 内部函数引用了外部函数的变量
- 外部函数执行后返回了内部函数
示例代码:
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const counter = outer(); // 闭包形成
counter(); // 1
counter(); // 2
二、闭包的应用场景
1. 封装私有变量
闭包常用于模拟私有变量的封装,防止外部直接修改:
function createCounter() {
let count = 0;
return {
increment() { count++; return count; },
decrement() { count--; return count; }
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.decrement()); // 0
2. 延长变量生命周期(缓存/记忆函数)
function memoize(fn) {
const cache = {};
return function(key) {
if (cache[key]) return cache[key];
const result = fn(key);
cache[key] = result;
return result;
};
}
3. 事件监听绑定中的变量保留
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 正确输出 0,1,2,因 let 有块级作用域
}, i * 1000);
}
如果使用 var,可以配合闭包解决变量提升问题:
for (var i = 0; i < 3; i++) {
(function(index){
setTimeout(() => console.log(index), index * 1000);
})(i);
}
4. 高阶函数/工厂函数/回调函数的典型场景
function withPrefix(prefix) {
return function (str) {
return prefix + str;
};
}
const hello = withPrefix("Hello, ");
console.log(hello("World")); // Hello, World
三、闭包的性能和内存问题
- 闭包会增加内存消耗,因为其引用的外部变量不会被销毁,造成 GC 回收延迟或内存泄漏
- 特别是在 DOM 操作、定时器、事件监听器中,如果闭包持有大量 DOM 引用,要注意手动解除引用或使用弱引用(如
WeakMap)
7. 解释以下代码的输出顺序:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
});
Promise.resolve().then(() => {
console.log('promise2');
});
console.log('script end');
题目要点
- JS 先执行所有同步代码(输出 script start 和 script end)
- 微任务(Promise 的
.then)优先于下一轮宏任务执行(输出 promise1、promise2) - 宏任务(setTimeout)最后执行(输出 setTimeout)
- 事件循环模型中:同步 → 微任务 → 宏任务 是关键执行顺序
参考答案
考察点
- 理解 JavaScript 事件循环(Event Loop) 的执行机制
- 掌握 宏任务(macrotask) 与 微任务(microtask) 的执行顺序
- 能准确分析同步任务、Promise、setTimeout 等在执行栈中的先后关系
参考答案
一、代码执行顺序分析
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
});
Promise.resolve().then(() => {
console.log('promise2');
});
console.log('script end');
二、执行流程详解
同步代码执行(立即执行):
console.log('script start'); // 输出:script start setTimeout(...); // 注册宏任务,等待执行 Promise.resolve().then(...); // 注册微任务 Promise.resolve().then(...); // 再注册微任务 console.log('script end'); // 输出:script end此时,输出顺序为:
script start script end微任务队列执行(在当前宏任务结束后,立即执行所有微任务):
- 执行
promise1 - 执行
promise2
输出:
promise1 promise2- 执行
宏任务队列执行:
- 执行
setTimeout回调
输出:
setTimeout- 执行
三、最终输出顺序
script start
script end
promise1
promise2
setTimeout
8. typeof null和null instanceof Object的结果是什么
题目要点
typeof null === 'object'是历史遗留的设计问题null instanceof Object === false因为 null 没有原型链- 类型判断建议优先使用
=== null、Object.prototype.toString.call(val)等更准确方式
参考答案
考察点
- 掌握 JavaScript 中
typeof与instanceof操作符的机制 - 能解释为何
typeof null返回'object'的历史原因 - 理解原型链与类型检测的区别
参考答案
一、typeof null 的结果
typeof null === 'object';
✅ 原因说明:
typeof是一个用于检测数据类型的操作符,但它有个历史遗留问题:- 在 JavaScript 最初设计时,
null被定义为一个对象的占位值 - 底层实现中,
null的二进制表示是0x00,而对象的类型标记也是以 0 开头 - 所以
typeof null错误地返回了'object',这是 ECMAScript 历史上的一个bug
- 在 JavaScript 最初设计时,
❗注意:
- 这个问题已经存在多年,但由于历史兼容性问题,ECMA 没有修复它
- 你可以通过更精确的方法判断 null,例如:
value === null
二、null instanceof Object 的结果
null instanceof Object === false;
✅ 原因说明:
instanceof检查的是原型链上是否能找到目标构造函数的 prototypenull是原始值,没有原型链,也不是任何构造函数的实例对象
因此:
null instanceof Object返回false- 而例如
{}、[]、new Date()等对象,都返回true
总结对比
| 表达式 | 结果 | 原因说明 |
|---|---|---|
typeof null | 'object' | 历史遗留 bug,null 被错误识别为对象 |
null instanceof Object | false | null 无原型链,不是 Object 的实例 |
9. React Fiber是什么?它是如何工作的
题目要点
Fiber是 React 的新的 协调与更新引擎,用于支持异步可中断渲染Fiber 拆分了更新过程,使得渲染任务可以被打断、恢复
整个 Fiber 架构包括三阶段:调度、渲染、提交
使用
requestIdleCallback等实现任务切片,提升性能通过 Fiber,React 实现了更强大的并发特性和用户响应性
Fiber Node / Effect List / Lanes / Scheduler 是核心关键词
参考答案
考察点
- 掌握 React 新架构 Fiber 的设计目的和核心机制
- 理解 Fiber 如何解决 React 16 之前的性能瓶颈
- 能准确说明 Fiber 的工作原理、任务调度、递归方式等关键设计点
- 理解 Fiber 和浏览器帧率调度的关系
参考答案
一、React Fiber 的定义与出现背景
Fiber 是 React 16 引入的重构版核心算法,用于实现更可控的更新过程。
简单理解:Fiber 是对 React 树的“增量更新”机制的支持,是一个可中断、可恢复的渲染算法。
为什么需要 Fiber?
React 15 及之前的 Reconciliation(协调)过程是同步、递归的:
- 一旦开始更新 Virtual DOM 树,中途无法中断
- 大型组件树会导致主线程长时间卡顿,影响动画、响应、输入
Fiber 带来的改变:
- 将递归的树更新过程拆分成可中断的任务单元
- 利用浏览器的空闲时间进行任务调度,实现高响应性(实现异步可中断渲染)
- 支持 React 的并发模式(Concurrent Mode),例如:
Suspense、useTransition
二、Fiber 的核心机制
1. Fiber Node
每个组件在更新时,都会生成一个对应的 Fiber 节点对象,形成一棵 Fiber 树:
FiberNode {
type, // 类型:函数组件、类组件、HostComponent等
key, // React key
return, // 父 Fiber
child, // 第一个子 Fiber
sibling, // 右边的兄弟 Fiber
alternate, // 当前 Fiber 对应的上一次 Fiber(旧树)
stateNode, // 真实 DOM 或 class 实例
effectTag, // 标记更新类型
...
}
2. Fiber 架构的三个阶段
React Fiber 的更新过程可分为三大阶段:
(1)调度阶段(reconciliation)
- 任务被插入调度队列,由
Scheduler调度执行 - React 利用
requestIdleCallback/MessageChannel实现任务时间切片 - 每次只处理一小段 Fiber 节点,中间可以中断恢复
此阶段只计算更新差异(diff),不会修改真实 DOM
(2)渲染阶段(render phase)
- 从根组件开始,构建新的 Fiber 树
- 标记所有需要更新的节点(
effectTag) - 生成 “副作用列表” (
effect list)
这部分也称为“协调阶段”,是纯函数阶段,没有副作用执行
(3)提交阶段(commit phase)
- 将所有副作用统一提交:更新 DOM、触发生命周期、Ref 等
- 此阶段是同步、不可中断的
三、Fiber 的调度模型
Fiber 的调度基于**优先级(Lanes)**设计:
- 每个更新任务都有一个优先级(如:同步更新、高优先交互、低优先动画)
- React 可以根据空闲时间选择合适的任务执行
- 实现任务的抢占、中断与恢复,提高整体响应性
四、React Fiber 的意义与优势
- ✅ 支持异步渲染与任务中断,避免长任务阻塞 UI
- ✅ 可根据优先级灵活调度任务(比如用户输入优先)
- ✅ 支持
Concurrent Mode并发特性 - ✅ 为后续功能如
Suspense、Transition、useDeferredValue提供技术支撑
10. useEffect和useLayoutEffect的区别是什么
题目要点
useLayoutEffect执行在浏览器绘制前,同步副作用useEffect执行在浏览器绘制后,异步副作用页面需读取/修改 DOM 布局时必须使用
useLayoutEffect不涉及 DOM 的副作用,如请求、事件监听等应使用
useEffect滥用
useLayoutEffect会引发性能问题执行时机不同 / 是否阻塞渲染 / 是否同步影响 DOM / 视觉稳定性
参考答案
考察点
- 掌握 React 中两个副作用钩子的执行时机和使用场景
- 理解 DOM 渲染流程及其与副作用的关系
- 能在项目中合理区分 useEffect 与 useLayoutEffect 的适用情况
- 理解它们对性能与视觉渲染的影响
参考答案
一、核心区别概览
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 组件渲染后,浏览器绘制之后 | 组件渲染后,浏览器绘制之前(同步) |
| 执行顺序 | 异步(放入宏任务队列) | 同步(与 DOM 渲染同步) |
| 阻塞页面绘制 | 否 | ✅ 是(会阻塞浏览器绘制) |
| 使用场景 | 异步请求、事件绑定、订阅等 | 读取 DOM 布局、同步 DOM 操作 |
二、原理说明
React 的组件渲染分为以下步骤:
- 构建虚拟 DOM
- 提交更新,渲染到真实 DOM
- 浏览器执行绘制
- 调用副作用函数
useLayoutEffect会在 DOM 更新后但浏览器绘制之前 执行,适用于读取 DOM 布局或同步修改 DOMuseEffect会在 浏览器绘制之后异步执行,不会阻塞页面渲染
三、使用场景对比
✅ useEffect 常用于:
- 发送异步请求
- 设置订阅、事件监听
- 定时器、日志上报
- 不涉及 DOM 读取和同步操作的副作用
useEffect(() => {
fetchData();
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
✅ useLayoutEffect 常用于:
- 读取 DOM 布局信息(如 offsetHeight)
- 同步修改 DOM 样式,防止闪动
useLayoutEffect(() => {
const height = ref.current.offsetHeight;
ref.current.style.transform = `translateY(-${height}px)`;
}, []);
如果用
useEffect做 DOM 操作,可能会出现“闪一下”的视觉问题,因为已经完成了绘制。
四、常见误区与面试陷阱
- ❌ 误用 useEffect 操作 DOM:可能会导致视觉闪烁或布局错位
- ❌ 用 useLayoutEffect 做重型任务:会阻塞页面渲染,影响性能
- ❌ 不了解执行顺序:两者都是在组件挂载后才执行,而非渲染前
小技巧:能用
useEffect就用它,useLayoutEffect留给需要同步控制 DOM 的场景
11. React中的虚拟DOM和Diff算法是如何提高性能的
题目要点
- 虚拟DOM是JS对象表示的DOM树,避免直接频繁操作真实DOM
- Diff算法通过同层对比、key优化、类型判断将比较复杂度降至O(n)
- 性能优化体现在减少真实DOM操作、最小化重绘、列表复用
- 列表更新场景中合理使用
key可以避免不必要重渲染 - 结合Fiber架构实现异步、可中断的更新机制,进一步提升大规模应用性能
参考答案
考察点
- React 的核心渲染机制(Virtual DOM)
- Diff 算法在性能优化中的作用
- 为什么需要虚拟DOM?如何高效比较变化?
- 面试中高频考察其底层原理、性能优势和适用场景
参考答案
一、什么是虚拟DOM(Virtual DOM)
虚拟DOM是用JavaScript 对象来描述DOM结构的轻量级副本,它不直接操作真实DOM,而是通过比较(Diff)和更新策略,实现高效的DOM变更。
const vNode = {
type: 'div',
props: { className: 'container' },
children: ['Hello']
};
虚拟DOM的主要优势:
- 避免直接频繁操作真实DOM(性能开销大)
- 可跨平台(浏览器 / Native / 小程序等)
- 支持高效更新的前提:Diff算法
二、React中虚拟DOM的工作机制
- 初次渲染:
JSX -> 虚拟DOM -> 真实DOM - 组件更新:
新的虚拟DOM与旧的虚拟DOM进行 Diff 比较 - 得到 最小差异补丁(Patch)
- 通过 Reconciliation 协议 更新真实DOM
React通过批量、分批地合并更新,避免频繁的DOM重绘和重排,提高性能。
三、Diff 算法的优化原理
React 的 Diff 算法并不是传统的 O(n³) 全树对比,而是做了以下优化降为 O(n):
1. 同层比较(树的同层节点之间对比,不跨层)
- 假设结构稳定,只在同一层做对比,跳过跨层节点。
2. Key机制优化列表对比
- 对于列表结构,如果没有
key,会使用下标作为标识,容易导致重渲染。 - 正确使用
key可避免不必要的节点移除 / 添加。
3. 节点类型不同,直接销毁重建
- 不再递归比较,节省计算
4. 组件类型不同,不做深比较,直接替换
四、代码示例与性能影响
假设有以下两个列表:
// 更新前
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
// 更新后
<ul>
<li key="b">B</li>
<li key="a">A</li>
<li key="c">C</li>
</ul>
- 如果没加
key,React 会以为每个节点都变了,导致重新渲染 - 有了
key后,它知道 A、B、C 节点只是顺序变化,不需要销毁重建
五、为什么虚拟DOM + Diff 能提升性能?
- 批处理更新:收集变更后统一更新
- 最小差异更新:通过Diff算法找到最小变更集
- 避免不必要渲染:精确定位需要更新的DOM
- 与 Fiber 架构结合:支持异步、可中断的渲染流程(React 16+)
12. React.memo的作用是什么
题目要点
React.memo是高阶组件,用于记忆函数组件,避免不必要的重新渲染- 内部默认使用
Object.is做 props 浅比较,也可以自定义比较函数 - 适用于纯函数组件、静态 props、高渲染成本组件
- 使用场景需评估 props 变更特性,避免滥用
- 类似作用的还有
useMemo、useCallback但用法不同
参考答案
考察点
- 组件性能优化手段
- React 的组件渲染机制
- React.memo 的使用场景和限制
- 如何避免组件的不必要重渲染
参考答案
一、React.memo 的原理与定义
React.memo 是一个高阶组件(Higher-Order Component),用于对函数组件进行“记忆化”处理,避免因父组件重新渲染而导致的子组件重复渲染。
const MemoizedComponent = React.memo(MyComponent);
- 本质上是对组件的 props 做浅比较,当 props 没有变化时,组件就不会重新渲染。
- 默认使用
Object.is+ 浅比较,但可以传入自定义比较函数。
二、使用场景
- 子组件是纯函数组件,只依赖 props 渲染。
- 子组件渲染成本高,频繁更新但 props 实际未变。
- 父组件更新但不影响子组件时,使用
React.memo可以跳过子组件渲染。
示例代码
const Child = React.memo(({ value }) => {
console.log("Child render");
return <div>{value}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child value="static" />
</>
);
}
- 每次点击按钮时,
Parent重新渲染,但由于Child的 props 没变,Child不会重新渲染。
三、自定义比较函数
默认是浅比较,如果传入的是对象类型的 props,推荐使用自定义比较器:
function areEqual(prevProps, nextProps) {
return prevProps.id === nextProps.id;
}
const MemoComp = React.memo(MyComponent, areEqual);
四、常见误区与限制
- 仅适用于函数组件(类组件用
PureComponent) - 不能阻止内部状态或上下文变更触发的更新
- 浅比较局限性:传入复杂对象(如函数或新对象字面量)会导致每次 props 都变化
- 过度使用可能适得其反:增加计算成本,不一定提升性能
13. CORS的几个关键头部是什么?它们的作用是什么
题目要点
Access-Control-Allow-Origin:指定允许访问的来源Access-Control-Allow-Methods:允许的跨域请求方法Access-Control-Allow-Headers:允许携带的自定义请求头Access-Control-Allow-Credentials:是否允许携带凭据,需配合具体 OriginAccess-Control-Expose-Headers:客户端可访问的响应头Access-Control-Max-Age:预检结果的缓存时间Origin是浏览器发送的标识来源的请求头- 区分简单请求与预检请求,理解触发条件和影响性能的机制
参考答案
考察点
- 理解 CORS(跨域资源共享)机制的本质与安全模型
- 熟悉 CORS 过程中的关键 HTTP 头部
- 掌握浏览器与服务端如何协同完成跨域请求
- 能解释预检请求与简单请求的异同
参考答案
一、什么是 CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享)是浏览器基于同源策略限制跨域请求的一种安全机制。
为允许安全的跨域请求,CORS 定义了一套服务端响应的HTTP头部规则,告知浏览器“这个跨域请求是允许的”。
二、CORS 关键头部及作用
Access-Control-Allow-Origin
- 作用:指定允许访问资源的来源(Origin)
- 示例:
Access-Control-Allow-Origin: https://example.com
- 支持通配符:
*表示允许任意来源(但不适用于携带 Cookie 的请求)
Access-Control-Allow-Methods
- 作用:列出服务端允许的跨域请求方法
- 在预检请求(OPTIONS)中响应
- 示例:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers
- 作用:声明哪些自定义请求头允许在跨域中使用(用于预检)
- 示例:
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials
作用:指示是否允许跨域请求携带 Cookie、HTTP认证信息等凭据
值必须为:
true配套限制:
Access-Control-Allow-Origin不能是*,必须是明确域名- 浏览器端需设置
xhr.withCredentials = true
Access-Control-Expose-Headers
- 作用:允许客户端 JS 脚本访问哪些响应头
- 默认客户端只能访问部分“安全响应头”,如
Content-Type、Cache-Control - 如需暴露自定义响应头,需要设置:
Access-Control-Expose-Headers: X-Custom-Header
Origin(请求头)
- 作用:由浏览器自动添加,用于标识发起请求的源(协议+域名+端口)
- 服务器通过此字段判断是否允许跨域
Access-Control-Max-Age
- 作用:指定预检请求的结果在缓存中保留的时间(秒)
- 可以减少频繁的预检请求:
Access-Control-Max-Age: 86400
三、简单请求 vs 预检请求(Preflight)
| 请求类型 | 触发条件 | 是否发送 OPTIONS | 常见场景 |
|---|---|---|---|
| 简单请求 | GET、POST、HEAD 且无自定义头 | 否 | 表单提交、图片加载等 |
| 预检请求 | 使用 PUT/DELETE/自定义头/Credentials | 是 | RESTful API、携带 Token 的请求等 |
14. 实现debounce函数(实现 debounce(fn, duration, leading) ,支持立即执行( leading=true )
题目要点
- 防抖是延迟执行,避免高频调用,适用于输入搜索、resize 等场景
leading=true表示首次立即执行,后续防抖时间内不再重复执行- 实现核心是清除
setTimeout+ 用hasCalled标记是否执行过 - 注意闭包、上下文(this)与参数传递的正确性
参考答案
考察点
- 理解防抖(debounce)的应用场景与机制
- 掌握闭包与定时器控制函数节流频率
- 实现一个支持
leading(立即执行) 的 debounce 版本 - 熟悉 JavaScript 高阶函数与异步任务调度
参考答案
一、什么是 debounce(防抖)?
**debounce(防抖)**是一种函数执行控制技术,它能在高频事件触发时,只在事件停止一段时间后执行回调函数,避免频繁执行逻辑。
- 常见场景:输入框搜索、窗口 resize、滚动 scroll、按钮点击防重复提交等。
二、为什么需要支持 leading=true?
默认的 debounce 是“等最后一次触发后”才执行回调。
而有些场景(如点击按钮),希望第一次就立即执行一次,这个就是 leading 参数的含义。
三、实现 debounce(fn, duration, leading)
function debounce(fn, duration = 300, leading = false) {
let timer = null;
let hasCalled = false;
return function (...args) {
const context = this;
// 如果 leading 为 true,且未执行过
if (leading && !hasCalled) {
fn.apply(context, args);
hasCalled = true;
}
// 清除上一次定时器
clearTimeout(timer);
timer = setTimeout(() => {
if (!leading) {
fn.apply(context, args);
}
hasCalled = false; // 重置标记,允许下次 leading 生效
}, duration);
};
}
四、使用示例
function onSearchInput(value) {
console.log("搜索关键词:", value);
}
// 300ms 防抖 + 立即执行
const debouncedInput = debounce(onSearchInput, 300, true);
// 模拟用户输入
debouncedInput("vue");
debouncedInput("vue3");
debouncedInput("vue3.x");
输出只会立即执行一次 "vue",其余触发会被 debounce 掉。
五、常见误区或陷阱
误区一:leading 模式会重复执行 → 实现中需正确使用标志位
hasCalled,避免后续 debounce 时间内重复触发。误区二:忽视 this 上下文和参数传递 → 实现中使用
apply(context, args)确保正确绑定调用者和传参。误区三:防抖和节流混淆 → 节流是固定时间段内执行一次;防抖是触发结束后延迟执行。