# 前端面试题
# 1. 如果现在要从零搭建一个项目,你会怎么做?
从零搭建项目需遵循 “规划→搭建→开发→优化→上线” 的闭环流程,核心是先明确目标再落地,避免后期架构重构。以下是分阶段关键步骤,覆盖技术选型、环境配置、业务开发等核心环节。
# 一、前期规划:定方向、避返工
前期规划决定项目基础走向,需明确核心目标与工具选型,避免盲目开发。
明确项目核心信息
- 项目类型:确认是 PC 端官网、移动端 H5、管理系统还是小程序。
- 核心功能:列出必做功能(如用户登录、数据列表、表单提交)与可选功能。
- 用户群体:若面向 C 端需考虑兼容性,面向 B 端(如管理系统)可优先保证功能完整性。
技术栈选型(按需匹配)
技术类别 常见选项及适用场景 前端框架 React(复杂交互,如电商)、Vue(轻量易上手,如官网)、Svelte(性能优先) 构建工具 Vite(热更新快,中小型项目)、Webpack(配置灵活,大型项目) 状态管理 Pinia(Vue 生态,轻量)、Redux Toolkit(React 生态,规范) 路由 Vue Router(Vue 生态)、React Router(React 生态) UI 组件库 Ant Design(中后台)、Element Plus(Vue 生态)、Tailwind CSS(自定义样式) 设计项目目录结构提前规划
src目录,保证代码可维护性,示例结构如下:src/ ├─ components/ # 公共组件(如按钮、弹窗) ├─ pages/ # 页面组件(如首页、登录页) ├─ api/ # 接口封装(统一管理接口地址和请求) ├─ utils/ # 工具函数(如时间格式化、权限判断) ├─ router/ # 路由配置(路由规则、守卫) ├─ store/ # 状态管理(全局数据存储) └─ styles/ # 全局样式(如变量、重置样式)1
2
3
4
5
6
7
8
# 二、环境搭建:搭基础、定规范
环境搭建是项目的 “地基”,需统一工具配置与开发规范,减少协作冲突。
- **初始化项目(用构建工具快速创建)**根据技术栈选择对应命令,生成基础框架:
- Vue + Vite:
npm create vite@latest 项目名 -- --template vue - React + Vite:
npm create vite@latest 项目名 -- --template react - React + Create React App:
npx create-react-app 项目名
- Vue + Vite:
- 配置开发规范(统一代码风格)
- 代码检查:安装
eslint(语法检查)和prettier(格式化),并创建配置文件(.eslintrc.js、.prettierrc),示例规则:- ESLint:禁止未声明变量、强制使用分号。
- Prettier:设置缩进 2 空格、单行最大长度 120 字符。
- Git 提交规范:用
husky + commitlint限制提交信息格式,如feat: 新增登录功能、fix: 修复表单校验bug。
- 代码检查:安装
- 引入核心依赖并配置安装项目必需依赖,并做基础配置:
- 接口请求:安装
axios,封装请求拦截(加 token)、响应拦截(统一错误处理)。 - 路由:安装
vue-router/react-router-dom,配置路由规则与登录守卫(未登录跳转登录页)。 - 状态管理:安装
pinia/@reduxjs/toolkit,初始化全局状态(如用户信息)。
- 接口请求:安装
# 三、核心开发:建骨架、填功能
开发阶段需先搭 “骨架” 再填业务,保证架构稳定后再做细节。
搭建基础架构(先实现核心能力)
全局样式:引入
normalize.css重置浏览器默认样式,定义全局 CSS 变量(如主题色--primary: #1890ff)。公共组件封装:开发高频复用组件(如加载中、弹窗、表单输入框),支持 Props 传参与事件触发。
接口统一管理:在
api目录按模块拆分接口(如api/user.js管理用户相关接口),示例:// api/user.js import request from '../utils/request' export const login = (data) => request({ url: '/login', method: 'post', data })1
2
3
开发业务模块(按优先级拆分)
- 优先开发基础页面:如登录页、首页,实现页面跳转与基础布局。
- 再开发复杂功能:如数据列表(分页、搜索)、表单提交(校验、提交 loading)、详情页(路由传参)。
- 联调后端接口:调用
api目录的接口,处理数据渲染(如列表渲染用v-for/map)与错误提示(如请求失败弹窗)。
适配与兼容性处理
- 移动端:用
postcss-pxtorem将 px 自动转为 rem,配合lib-flexible实现多屏幕适配。 - 浏览器兼容:用
@babel/preset-env+core-js处理 ES6 + 语法,保证在 IE11 等低版本浏览器正常运行。
- 移动端:用
# 四、优化与测试:提性能、保质量
上线前需做性能优化与测试,避免线上问题。
- 项目性能优化
- 打包优化:Vite/Webpack 配置代码分割(
splitChunks)、Tree Shaking(删除无用代码),减小包体积。 - 加载优化:实现路由懒加载(如
const Home = () => import('./pages/Home'))、图片懒加载(用vue-lazyload或原生loading="lazy")。 - 体验优化:加页面加载动画、按钮点击反馈、表单实时校验提示。
- 打包优化:Vite/Webpack 配置代码分割(
- 测试验证(多维度检查)
- 单元测试:用
jest测试工具函数(如时间格式化函数)和基础组件(如按钮点击事件)。 - 手动测试:检查功能完整性(登录、提交、跳转是否正常)、兼容性(Chrome/Firefox/Safari)、响应式(手机 / 平板 / PC)。
- 单元测试:用
# 五、部署上线:推生产、做监控
部署是项目落地的最后一步,需确保线上稳定运行。
构建生产包执行打包命令生成
dist文件夹(生产环境代码):Vite:
npm run buildWebpack:
npm run build打包后检查
dist目录大小,重点看vendor.js(第三方依赖)是否过大,必要时进一步优化。
选择部署平台(按需选择)
- 静态项目(如官网、H5):部署到 Vercel(自动 CI/CD)、Netlify、阿里云 OSS(配合 CDN 加速)。
- 需后端服务的项目:部署到阿里云 ECS、腾讯云服务器,配置 Nginx 反向代理(解决跨域)。
上线后监控
- 错误监控:接入
Sentry,捕获前端报错(如 JS 错误、接口 404),并实时告警。 - 数据监控:接入百度统计、Google Analytics,分析用户访问量、页面停留时间,优化产品体验。
- 错误监控:接入
# *2. 什么是闭包?
闭包的核心定义是 “有权访问另一个函数作用域内变量的函数”,通常由 “函数嵌套 + 内层函数引用外层变量” 形成。
它的主要应用场景有 3 个:
实现数据私有,比如模块化中隐藏内部变量,只暴露方法(如
function module(){ let a=1; return {getA(){return a}} });延长变量生命周期,比如防抖节流函数中保存定时器 ID;
实现柯里化,比如
add(1)(2)=3的函数封装。
潜在问题是 “内存泄漏”,因为闭包引用的外层变量不会被 GC 回收,解决方式是 “不再使用时主动将引用设为 null”。
# *3. ES6新增了什么功能?
变量声明:
let(块级作用域,不可重复声明)、const(声明常量,块级作用域),替代var解决作用域问题。箭头函数:
() => {}简化函数写法,不绑定自身this(继承外层上下文this)。解构赋值:快速提取数组 / 对象属性,如
const { a, b } = obj; const [x, y] = arr;。模板字符串:反引号包裹,支持多行文本和变量插入。
`${变量}`1类与继承:
class语法糖(替代原型链),extends实现继承,含constructor、super等。模块系统:
import导入、export导出模块,支持模块化开发。Promise:异步编程解决方案,避免回调地狱,支持
then/catch链式调用。新数据结构:
Set(无重复值集合)、Map(键值对集合,键可任意类型)。函数增强:默认参数(
function fn(a=1) {})、剩余参数(...args)、扩展运算符(...展开数组 / 对象)。其他:
for...of循环(遍历可迭代对象)、Symbol(唯一值类型)、Proxy(对象代理)等。
# *4. 什么是Promise?
定义:JavaScript 中用于处理异步操作的对象,代表一个异步操作的最终完成(或失败)及其结果值。
核心特性:
- 三种状态:
pending(初始状态)、fulfilled(操作成功)、rejected(操作失败),状态一旦改变(从pending到fulfilled或rejected)则不可逆。 - 链式调用:通过
then()处理成功结果、catch()处理失败、finally()执行无论成功失败都需的逻辑,避免 “回调地狱”(嵌套回调导致的代码混乱)。
- 三种状态:
常用静态方法:
Promise.resolve(value):快速创建一个成功状态的 Promise;Promise.reject(error):快速创建一个失败状态的 Promise;Promise.all(iterable):等待所有 Promise 成功,返回结果数组;若有一个失败则立即失败;Promise.race(iterable):返回第一个完成(成功或失败)的 Promise 的结果。Promise.allSettle(iterable):等待所有 Promise 都完成(无论成功或失败),最终返回一个始终成功的 Promise,结果是一个数组,包含每个 Promise 的详细状态和结果。Promise.any(iterable):返回第一个成功的 Promise 的结果。
方法对比与应用场景:
● Promise.all:适用于“所有任务都成功才继续”的场景(如并行请求多个接口,全部返回后渲染页面)。
● Promise.allSettled:适用于“需要知道所有任务结果”的场景(如批量上传文件,无论成功失败都记录结果)。
● Promise.race:适用于“取最快响应”的场景(如请求超时控制)。
● Promise.any:适用于“至少一个成功”的场景(如多源数据获取,只要一个可用就返回)。
作用:规范异步代码写法,使异步逻辑更清晰、易维护。
# 5. 接口超时会进入Promise的catch回调吗?
核心结论:取决于是否将超时逻辑转化为
Promise.reject()。具体说明:
Promise 本身不内置超时处理,接口超时不会自动触发
catch。需手动实现超时逻辑(如用
Promise.race结合setTimeout),当超时时主动调用reject,此时会进入catch。示例(原生 fetch 超时处理):
const request = fetch('api/data'); const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('超时')), 5000) ); Promise.race([request, timeout]) .then(res => {}) .catch(err => { /* 超时会进入这里 */ });1
2
3
4
5
6
7部分请求库(如 axios)内置
timeout配置,超时时会自动reject,因此会进入catch。
简言之:超时需显式转为 rejected 状态才会触发 catch,否则不会。
# *6. 宏任务与微任务。
定义:JavaScript 异步任务的两种分类,由事件循环(Event Loop)按优先级调度执行。
宏任务(Macro Task)
- 包含:
script整体代码、setTimeout、setInterval、I/O 操作(如接口请求)、UI 渲染、setImmediate(Node 环境)。 - 特点:优先级较低,执行间隔较长,每次事件循环仅执行一个宏任务。
- 包含:
微任务(Micro Task)
- 包含:
Promise.then/catch/finally、async/await(本质是 Promise 语法糖)、queueMicrotask、process.nextTick(Node 环境,优先级高于 Promise)。 - 特点:优先级高于宏任务,当前宏任务执行完毕后,会清空所有微任务队列再执行下一个宏任务。
- 包含:
执行顺序:
- 执行同步代码(属于当前宏任务);
- 同步代码执行完,检查并执行所有微任务(按入队顺序);
- 微任务执行完,执行 UI 渲染(若有);
- 从宏任务队列取一个任务执行,重复步骤 1-3(事件循环)。
示例:
console.log('同步代码'); // 同步执行 setTimeout(() => console.log('宏任务 setTimeout'), 0); // 宏任务队列 Promise.resolve().then(() => console.log('微任务 then')); // 微任务队列 // 输出顺序:同步代码 → 微任务 then → 宏任务 setTimeout1
2
3
4
# 7. 如何判断数组?
Array.isArray()- 用法:
Array.isArray(arr) - 优点:ES5 新增的标准方法,专门用于判断数组,最可靠、简洁。
- 缺点:不支持 IE8 及以下(可通过 polyfill 兼容)。
- 用法:
Object.prototype.toString.call()- 用法:
Object.prototype.toString.call(arr) === '[object Array]' - 优点:兼容性极好(支持所有浏览器),不受全局环境影响(如 iframe 中创建的数组也能正确判断)。
- 缺点:写法稍繁琐。
- 用法:
instanceof操作符- 用法:
arr instanceof Array - 优点:简单直观。
- 缺点:若数组在不同全局环境(如 iframe)中创建,因
Array构造函数不同,会导致判断失效(返回false)。
- 用法:
constructor属性- 用法:
arr.constructor === Array - 优点:写法简单。
- 缺点:
constructor可被手动修改(如arr.constructor = Object),导致判断不可靠,不推荐使用。
- 用法:
推荐方案:优先使用 Array.isArray()(现代环境),需兼容旧环境时用 Object.prototype.toString.call()。
# 8. call、apply、bind的区别。
三者均用于改变函数执行时的 this 指向,核心区别在于参数传递、执行时机和返回值:
- 参数传递
call(thisArg, arg1, arg2, ...):接收多个参数,第一个为this指向的对象,后续为函数的参数列表(逗号分隔)。apply(thisArg, [argsArray]):接收两个参数,第一个为this指向的对象,第二个为参数数组(或类数组对象,如arguments)。bind(thisArg, arg1, arg2, ...):参数格式同call,支持分阶段传参(先传部分参数,后续调用新函数时补传剩余参数)。
- 执行时机
call/apply:立即执行原函数。bind:不立即执行,返回一个绑定了this和部分参数的新函数,需手动调用新函数才执行。
- 返回值
call/apply:返回原函数执行的结果。bind:返回新的函数(已绑定this和预设参数)。
示例:
function fn(a, b) { console.log(this.x, a + b); }
const obj = { x: 10 };
fn.call(obj, 1, 2); // 立即执行,输出 10 3(this指向obj,参数1、2)
fn.apply(obj, [1, 2]); // 立即执行,输出 10 3(参数为数组[1,2])
const boundFn = fn.bind(obj, 1); // 返回新函数,绑定this为obj,预设参数1
boundFn(2); // 调用新函数,输出 10 3(补传参数2)
2
3
4
5
6
7
# 9. 什么是事件委托?
定义:又称事件代理,利用 事件冒泡机制,将子元素的事件处理逻辑绑定到父元素上,由父元素统一处理子元素的事件。
原理:事件触发后会从子元素向上冒泡至父元素,父元素通过事件对象的
target属性(指向实际触发事件的子元素),判断具体触发源并执行对应逻辑。优点:
- 减少事件监听器数量(如列表 100 个项,仅需给父元素绑定 1 个事件,而非 100 个),优化性能;
- 动态新增的子元素无需重新绑定事件,自动响应(因事件由父元素统一处理)。
示例:
// 父元素ul代理子元素li的点击事件 document.querySelector('ul').addEventListener('click', (e) => { if (e.target.tagName === 'LI') { // 判断触发源是li console.log('点击了li:', e.target.textContent); } });1
2
3
4
5
6
# 10. 如何解决跨域?
跨域由浏览器同源策略(协议、域名、端口任一不同即跨域)导致,常见解决方式:
CORS(跨域资源共享)
- 原理:服务器端通过设置响应头(如
Access-Control-Allow-Origin: *或指定域名)允许跨域请求。 - 适用:前后端分离项目,需后端配合配置,支持所有 HTTP 方法,最推荐的现代方案。
- 原理:服务器端通过设置响应头(如
JSONP
- 原理:利用
<script>标签不受同源策略限制的特性,动态创建 script 标签,通过回调函数获取数据(仅支持 GET 请求)。 - 适用:兼容性要求高的场景(如旧浏览器),但功能有限(仅 GET),已逐渐被 CORS 替代。
- 原理:利用
代理服务器
原理:前端请求同源的代理服务器,由代理服务器转发请求到目标跨域服务器(服务器间无跨域限制)。
场景:开发环境(如 Webpack DevServer 配置
proxy)、生产环境(Nginx 反向代理)。示例(Webpack):
devServer: { proxy: { '/api': { target: 'https://跨域域名', changeOrigin: true } } }1
postMessage
- 原理:用于不同域名的页面(如 iframe 嵌套)间通信,通过
window.postMessage发送数据,监听message事件接收。 - 适用:页面间跨域通信(如父页面与子 iframe 交互)。
- 原理:用于不同域名的页面(如 iframe 嵌套)间通信,通过
WebSocket
- 原理:WebSocket 协议(
ws:///wss://)本身不限制跨域,建立连接后可双向通信。 - 适用:实时通信场景(如聊天、实时数据更新)。
- 原理:WebSocket 协议(
优先推荐:CORS(简单高效)、代理服务器(开发环境便捷)。
# *11. TypeScript函数的参数类型都有哪些?
在 TypeScript 中,函数参数的类型可以是任意合法的 TypeScript 类型,核心可分为以下几类,涵盖基础类型、复合类型、特殊场景类型等:
# 1. 基础类型参数
即 TypeScript 支持的原始类型(number、string、boolean 等),直接约束参数的基础数据类型。
// 数字类型参数
function add(a: number, b: number): number {
return a + b;
}
// 字符串类型参数
function greet(name: string): string {
return `Hello, ${name}`;
}
// 布尔类型参数
function toggle(flag: boolean): string {
return flag ? "On" : "Off";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2. 对象类型参数
参数为对象(包括普通对象、数组、接口 / 类型别名定义的对象等),需明确对象的结构(属性及类型)。
# (1)匿名对象类型
直接在参数中定义对象结构:
function printUser(user: { name: string; age: number }): void {
console.log(`Name: ${user.name}, Age: ${user.age}`);
}
printUser({ name: "Alice", age: 20 }); // 正确
2
3
4
# (2)接口 / 类型别名定义的对象
通过 interface 或 type 复用对象类型:
interface User {
name: string;
age?: number; // 可选属性
}
function getUserInfo(user: User): string {
return `Name: ${user.name}, Age: ${user.age || "Unknown"}`;
}
type Point = { x: number; y: number };
function distance(p1: Point, p2: Point): number {
return Math.hypot(p2.x - p1.x, p2.y - p1.y);
}
2
3
4
5
6
7
8
9
10
11
12
# (3)数组类型参数
参数为数组,需指定元素类型:
// 普通数组
function sum(numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// 元组(固定长度和类型的数组)
function formatTuple(tuple: [string, number]): string {
return `${tuple[0]}: ${tuple[1]}`;
}
2
3
4
5
6
7
8
9
# 3. 联合类型参数
参数可以是多种类型中的一种(用 | 分隔),需注意在函数内部通过类型收窄处理不同类型。
// 参数可以是 string 或 number
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // 类型收窄为 string
} else {
return value.toFixed(2); // 类型收窄为 number
}
}
formatValue("hello"); // "HELLO"
formatValue(3.1415); // "3.14"
2
3
4
5
6
7
8
9
10
# 4. 可选参数
通过 ? 标记参数为可选(可传可不传),可选参数的类型会自动包含 undefined。
// age 是可选参数(类型:number | undefined)
function introduce(name: string, age?: number): string {
if (age) {
return `I'm ${name}, ${age} years old.`;
}
return `I'm ${name}.`;
}
introduce("Bob"); // 正确(不传 age)
introduce("Bob", 25); // 正确(传 age)
2
3
4
5
6
7
8
9
注意:可选参数需放在必选参数后面(除非用默认参数)。
# 5. 默认参数
为参数设置默认值(= 赋值),默认参数自动视为可选参数,TypeScript 会自动推断其类型(也可显式声明)。
// count 有默认值 1(类型:number)
function repeat(str: string, count: number = 1): string {
return str.repeat(count);
}
repeat("a"); // "a"(使用默认值 1)
repeat("a", 3); // "aaa"
2
3
4
5
6
特点:默认参数可放在必选参数前(但调用时需显式传 undefined 触发默认值)。
# 6. 剩余参数
通过 ... 收集多个参数为数组(通常用于不定长参数),需指定数组元素类型。
// 剩余参数 nums 是 number[] 类型
function sumAll(first: number, ...nums: number[]): number {
return first + nums.reduce((a, b) => a + b, 0);
}
sumAll(1, 2, 3); // 6(1 + 2 + 3)
sumAll(10); // 10(仅 first 参数)
2
3
4
5
6
# 7. 函数类型参数(回调函数)
参数本身是函数(如回调函数),需指定其参数类型和返回值类型。
// 回调函数参数:(n: number) => string
function processNumbers(
numbers: number[],
callback: (n: number) => string
): string[] {
return numbers.map(callback);
}
// 传入回调:将数字转为字符串
processNumbers([1, 2], (n) => `#${n}`); // ["#1", "#2"]
2
3
4
5
6
7
8
9
# 8. 泛型参数
参数类型为泛型(T、U 等),可动态适配不同类型,保持输入输出类型一致。
// 泛型参数 T:输入和返回值类型相同
function identity<T>(arg: T): T {
return arg;
}
identity("hello"); // 类型:string(自动推断 T 为 string)
identity<number>(123); // 类型:number(显式指定 T)
2
3
4
5
6
# 9. 其他特殊类型参数
null/undefined:参数只能是null或undefined(需开启strictNullChecks)。function handleNull(value: null): void { console.log("Value is null"); }1
2
3枚举类型:参数为枚举成员。
enum Status { Success, Error } function logStatus(status: Status): void { console.log(status === Status.Success ? "OK" : "Fail"); } logStatus(Status.Success); // "OK"1
2
3
4
5never类型:参数类型为never(表示参数永远不会被传递,极少用)。
# 总结
TypeScript 函数参数类型覆盖了从基础类型到复合类型、从固定类型到动态泛型的各种场景,核心是通过类型约束确保参数的合法性,同时兼顾灵活性(如联合类型、泛型)和可读性(如接口定义的对象类型)。实际开发中需根据参数的具体用途选择合适的类型,平衡类型安全和开发效率。
# *12. Type和Interface的区别。
在 TypeScript 中,type(类型别名)和 interface(接口)都用于描述类型,但它们的设计目的和功能有显著区别,核心差异体现在定义范围、扩展方式、合并特性等方面。以下是具体对比:
# 1. 定义范围:type 更灵活,interface 专注于对象类型
type(类型别名):可以为任意类型创建别名,包括基本类型(如number)、联合类型、交叉类型、元组、函数等。// 基本类型别名 type Age = number; // 联合类型 type Status = "success" | "error" | "pending"; // 函数类型 type Add = (a: number, b: number) => number; // 元组 type Point = [x: number, y: number];1
2
3
4
5
6
7
8
9
10
11interface(接口):仅用于描述对象类型(包括对象、数组、函数对象等),无法为基本类型、联合类型等创建接口。// 描述对象 interface User { name: string; age: number; } // 描述函数对象(函数的结构) interface Multiply { (a: number, b: number): number; }1
2
3
4
5
6
7
8
9
10
# 2. 扩展方式:interface 用 extends,type 用交叉类型(&)
两者都支持扩展,但语法和行为不同:
interface扩展:通过extends关键字,可扩展另一个interface或type(对象类型)。interface Animal { name: string; } // 扩展接口 interface Dog extends Animal { bark: () => void; } // Dog 类型:{ name: string; bark: () => void } // 扩展类型别名(对象类型) type Cat = { meow: () => void; }; interface PetCat extends Cat { age: number; } // PetCat 类型:{ meow: () => void; age: number }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type扩展:通过交叉类型(&)合并多个类型,生成新类型。type Car = { brand: string; }; // 交叉类型扩展 type ElectricCar = Car & { battery: number; }; // ElectricCar 类型:{ brand: string; battery: number }1
2
3
4
5
6
7
8
9
# 3. 合并特性:interface 支持自动合并,type 不支持
interface重复声明会自动合并:多次定义同名接口,TypeScript 会将它们的属性合并(冲突时需类型兼容)。这一特性常用于扩展第三方库的类型(如给window增加属性)。// 第一次声明 interface Config { url: string; } // 第二次声明(自动合并) interface Config { timeout: number; } // 合并后:{ url: string; timeout: number } const config: Config = { url: "api", timeout: 5000 };1
2
3
4
5
6
7
8
9
10
11
12type重复声明会报错:类型别名不可重复定义,否则会触发类型冲突。type Name = string; type Name = number; // 报错:标识符“Name”重复1
2
# 4. 实现(implements):class 对两者的支持有差异
class 可以通过 implements 实现 interface 或 type(对象类型),但 type 若为联合类型则无法实现。
实现
interface:完全支持。interface Runnable { run: () => void; } class Robot implements Runnable { run() { console.log("Running"); } // 正确实现 }1
2
3
4
5
6实现
type(对象类型):支持。type Flyable = { fly: () => void; }; class Bird implements Flyable { fly() { console.log("Flying"); } // 正确实现 }1
2
3
4
5
6实现
type(联合类型):不支持(联合类型无法被类具体实现)。type Moveable = { run: () => void } | { fly: () => void }; class Animal implements Moveable { // 报错:类“Animal”无法正确实现“Moveable”(联合类型无法被单一类实现) run() {} }1
2
3
4
5
# 5. 映射类型:type 更适配,interface 有限制
映射类型(如遍历 keyof 生成新类型)通常与 type 配合使用,interface 无法直接定义映射类型(但可扩展映射类型生成的结果)。
// 用 type 定义映射类型(将属性转为只读)
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
interface Book {
title: string;
}
type ReadonlyBook = Readonly<Book>; // { readonly title: string }
2
3
4
5
6
7
8
9
# 总结:如何选择?
- 优先用
interface:- 定义对象的结构(如 API 响应、组件 props)。
- 需要类型自动合并(如扩展第三方类型)。
- 希望代码更符合 OOP 风格(类实现接口)。
- 优先用
type:- 定义基本类型、联合类型、交叉类型、元组等非对象类型。
- 需要使用映射类型生成新类型。
- 不需要类型自动合并(避免意外覆盖)。
两者核心区别可概括为:interface 是 “对象结构的契约”,支持合并和扩展;type 是 “任意类型的别名”,更灵活但不支持合并。在多数场景下,两者可以互换,但根据上述特性选择更贴合的工具能让代码更清晰。
# 13. TypeScript的高阶用法。
# 1. 泛型约束(Generic Constraints)
通过 extends 限制泛型的范围,确保传入的类型满足特定条件。
// 约束 T 必须包含 length 属性
type HasLength = { length: number };
function getLength<T extends HasLength>(arg: T): number {
return arg.length;
}
getLength("abc"); // 正确(string 有 length)
getLength(123); // 错误(number 无 length)
2
3
4
5
6
7
# 2. 泛型默认值(Default Type Parameters)
为泛型指定默认类型,简化调用时的类型传递。
// 若未指定 T,默认为 string
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
createArray(3, "a"); // string[](无需显式传 T)
createArray<number>(3, 1); // number[](显式指定 T)
2
3
4
5
6
# 3. 泛型推断(Type Inference with Generics)
利用 infer 在条件类型中提取类型信息(常用于 “类型提取”)。
// 提取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
type Fn = () => number;
type FnReturn = ReturnType<Fn>; // number(自动推断函数返回值)
2
3
4
# 二、条件类型(Conditional Types)
类似 JavaScript 的 if-else,根据条件动态返回不同类型,支持分布式特性(对联合类型自动分发处理)。
# 1. 基础条件类型
// 若 T 是 string 则返回 number,否则返回 boolean
type CheckType<T> = T extends string ? number : boolean;
type A = CheckType<string>; // number
type B = CheckType<number>; // boolean
2
3
4
# 2. 分布式条件类型(对联合类型生效)
当泛型为联合类型时,条件类型会自动 “拆分” 联合项单独处理,再合并结果。
type Split<T> = T extends string ? "str" : "other";
type Union = string | number | boolean;
type Result = Split<Union>; // "str" | "other"(string→"str",number/boolean→"other")
2
3
# 3. 内置条件类型(常用工具类型)
TypeScript 内置了多个基于条件类型的工具类型,可直接复用:
Extract<T, U>:从 T 中提取可赋值给 U 的类型Exclude<T, U>:从 T 中排除可赋值给 U 的类型NonNullable<T>:排除 T 中的 null 和 undefined
type T = "a" | "b" | "c";
type U = "a" | "d";
type Extracted = Extract<T, U>; // "a"(T 中与 U 重叠的类型)
type Excluded = Exclude<T, U>; // "b" | "c"(T 中与 U 不重叠的类型)
type NotNull = NonNullable<string | null | undefined>; // string
2
3
4
5
# 三、映射类型(Mapped Types)
通过遍历已有类型的键(keyof),生成新的类型(类似 “类型层面的 for 循环”),常用于批量修改类型属性。
# 1. 基础映射类型
interface User {
name: string;
age: number;
}
// 将 User 的所有属性转为可选
type PartialUser = {
[K in keyof User]?: User[K]; // K 遍历 User 的所有键(name/age)
};
// 等价于:{ name?: string; age?: number }
2
3
4
5
6
7
8
9
10
# 2. 内置映射类型
TypeScript 内置了常用映射类型:
Partial<T>:所有属性变为可选Required<T>:所有属性变为必填Readonly<T>:所有属性变为只读
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number }
2
# 3. 高级映射:修改键名与属性
通过 as 实现键名重映射(TypeScript 4.1+),结合模板字面量类型生成动态键名。
// 将属性名转为“get+大写首字母”形式(如 name → getName)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
2
3
4
5
6
7
# 四、模板字面量类型(Template Literal Types)
类似 JavaScript 模板字符串,在类型层面拼接字符串,用于生成动态字符串类型。
# 1. 基础用法
type Greeting = `Hello, ${string}`; // 匹配 "Hello, " 开头的字符串
const g1: Greeting = "Hello, TypeScript"; // 正确
const g2: Greeting = "Hi, TypeScript"; // 错误(不匹配前缀)
2
3
# 2. 结合映射类型生成事件类型
type Events = "click" | "scroll" | "resize";
// 生成事件处理函数类型(如 onClick, onScroll)
type EventHandlers = {
[E in Events as `on${Capitalize<E>}`]: (e: Event) => void;
};
// { onClick: (e: Event) => void; onScroll: (e: Event) => void; ... }
2
3
4
5
6
# 五、递归类型(Recursive Types)
类型可以引用自身,用于描述嵌套结构(如树形结构、JSON 数据)。
// 定义 JSON 数据类型(支持嵌套)
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[] // 数组元素仍为 JSONValue
| { [key: string]: JSONValue }; // 对象值仍为 JSONValue
const data: JSONValue = {
name: "TS",
age: 5,
tags: ["a", "b"],
nested: { x: 1 } // 支持嵌套对象
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 六、类型守卫(Type Guards)
通过自定义函数缩小变量的类型范围(返回 param is Type 谓词),提高类型判断的灵活性。
interface Cat { type: "cat"; meow: () => void }
interface Dog { type: "dog"; bark: () => void }
// 类型守卫:判断是否为 Cat
function isCat(animal: Cat | Dog): animal is Cat {
return animal.type === "cat";
}
const pet: Cat | Dog = { type: "cat", meow: () => {} };
if (isCat(pet)) {
pet.meow(); // 正确(TypeScript 知道 pet 是 Cat)
} else {
pet.bark(); // 正确(pet 是 Dog)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 七、声明合并(Declaration Merging)
合并多个同名类型 / 接口的定义,常用于扩展第三方库的类型。
// 扩展内置 String 接口
interface String {
toDouble: () => string;
}
// 实现扩展的方法
String.prototype.toDouble = function() {
return this + this;
};
"a".toDouble(); // "aa"(TypeScript 识别扩展的方法)
2
3
4
5
6
7
8
9
# 14. Sass有什么接口?
Sass(CSS 预处理器)的 “接口” 更多指其提供的语法特性、内置功能和规则,用于增强 CSS 的可编程性和复用性。以下是 Sass 的核心功能(可理解为其提供的 “接口”):
# 1. 变量(Variables)
功能:定义可复用的值(如颜色、尺寸、字体),方便全局维护。
语法:用
$声明,支持作用域(局部 / 全局)。示例:
$primary-color: #3498db; // 声明变量 .button { color: $primary-color; // 使用变量 }1
2
3
4
# 2. 嵌套规则(Nesting)
功能:允许 CSS 选择器嵌套,模拟 HTML 结构,减少代码冗余。
支持:选择器嵌套、属性嵌套(如
font-系列)、父选择器引用(&)。示例:
.nav { ul { margin: 0; } // 嵌套子选择器 &:hover { color: red; } // & 指代父选择器 .nav font: { // 属性嵌套 size: 16px; weight: bold; } }1
2
3
4
5
6
7
8
# 3. 混合宏(Mixins)
功能:定义可复用的样式块,支持参数(含默认值),类似 “函数”。
语法:
@mixin定义,@include调用。示例:
@mixin flex-center($direction: row) { // 带默认参数的混合宏 display: flex; flex-direction: $direction; justify-content: center; align-items: center; } .box { @include flex-center(column); // 调用并传参 }1
2
3
4
5
6
7
8
9
# 4. 继承(Inheritance)
功能:通过
@extend复用另一个选择器的样式,生成更简洁的 CSS(合并选择器)。示例:
.base-btn { padding: 8px 16px; } .primary-btn { @extend .base-btn; // 继承 .base-btn 样式 background: blue; } // 编译后:.base-btn, .primary-btn { padding: 8px 16px; }1
2
3
4
5
6
# 5. 内置函数(Built-in Functions)
功能:提供大量预定义函数,处理颜色、字符串、数字、列表等。
- 颜色函数:
lighten($color, 10%)(提亮)、darken()(变暗)、rgba()等。 - 字符串函数:
to-upper-case($str)(转大写)、str-length()(长度)等。 - 数字函数:
round(3.2)(四舍五入)、percentage(0.5)(转百分比)等。
- 颜色函数:
示例:
$color: #3498db; .box { background: lighten($color, 20%); // 提亮颜色 width: percentage(0.8); // 输出 80% }1
2
3
4
5
# 6. 模块化(Modules)
功能:通过
@use和@forward管理样式文件,替代旧版@import(避免变量污染)。示例:
// _variables.scss(下划线表示局部文件) $font-size: 14px; // main.scss @use 'variables' as v; // 导入模块并别名 .text { font-size: v.$font-size; // 使用模块变量 }1
2
3
4
5
6
7
8
# 7. 控制指令(Control Directives)
功能:提供条件判断和循环,实现动态样式生成(类似编程语言逻辑)。
@if/@else:条件判断。@for:循环(@for $i from 1 through 3)。@each:遍历列表(@each $item in $list)。@while:循环(满足条件时执行)。
示例:
@for $i from 1 through 3 { .col-#{$i} { // #{} 插值语法 width: 100% / 3 * $i; } } // 编译后生成 .col-1, .col-2, .col-3 样式1
2
3
4
5
6
这些功能是 Sass 的核心 “接口”,通过它们可以实现 CSS 的模块化、复用性和动态生成,大幅提升样式开发效率。
# 15. WebSocket、SSE、轮询的选型与重连/心跳设计。
# 一、三种技术的核心特点与选型依据
三者均用于实现客户端与服务器的实时通信,但适用场景不同,核心差异如下:
| 技术 | 通信方向 | 协议基础 | 实时性 | 优势 | 劣势 | 典型场景 |
|---|---|---|---|---|---|---|
| 短轮询 | 客户端主动请求 | HTTP | 低 | 实现简单,兼容性极好(所有浏览器) | 无效请求多(空响应),服务器压力大 | 对实时性要求极低的场景(如定时刷新数据) |
| 长轮询 | 客户端请求 + 服务器 hold | HTTP | 中 | 减少无效请求(服务器有数据才响应) | 服务器需维持连接,资源消耗较高 | 实时性一般的场景(如早期即时通讯) |
| SSE | 服务器单向推送 | HTTP | 高 | 原生支持自动重连,轻量(文本传输) | 仅单向(客户端→服务器需另开请求) | 服务器主动推送场景(如实时通知、股票行情) |
| WebSocket | 全双工(双向) | 独立 WebSocket 协议 | 极高 | 持久连接,双向低延迟,开销小 | 实现稍复杂,需处理握手 / 断开逻辑 | 双向实时交互(如聊天、在线协作、游戏) |
选型原则:
- 通信方向:双向交互必选 WebSocket;仅服务器推选用 SSE(更轻量)。
- 实时性要求:毫秒级实时选 WebSocket;秒级延迟可选 SSE / 长轮询;非实时选短轮询。
- 兼容性:若需兼容极旧浏览器(如 IE8-),退选轮询;现代浏览器优先 WebSocket/SSE。
- 服务器压力:高并发场景下,WebSocket(持久连接)比轮询(频繁建连)更优;SSE 比长轮询更轻量。
# 二、重连设计(通用逻辑 + 技术适配)
当连接异常断开(如网络波动、服务器重启),需自动重连以恢复通信,核心设计如下:
# 1. 通用重连逻辑
- 断开检测:
- WebSocket:监听
onclose事件(event.code非 1000/1001 表示异常断开)。 - SSE:监听
onerror事件(或readyState变为CLOSED)。 - 轮询:连续多次请求失败(如 HTTP 503 / 超时)。
- WebSocket:监听
- 重试策略:
- 指数退避:避免频繁重试压垮服务器,设置一个初始重连延迟(如 1s),如果失败,下一次延迟翻倍(2s, 4s, 8s...),并设置一个最大延迟上限(如 60s)。
- 随机抖动:在计算出的延迟上增加一个小的随机值,防止所有掉线的客户端在同一时刻“风暴般”地重连服务器。
- 最大重试次数:设置阈值(如 10 次),超过后提示用户手动重连(避免无限重试)。
- 网络恢复检测:通过
navigator.onLine监听客户端网络状态,网络恢复后立即重试。
- 状态保存:重连期间缓存客户端待发送的消息(如 WebSocket 的未发送数据),重连成功后补发。
# 2. 技术适配细节
- WebSocket:断开后调用
new WebSocket(url)重建连接,需重新触发握手流程;重连成功后需重新订阅事件(如服务器端的主题订阅)。 - SSE:浏览器原生支持自动重连(通过
EventSource的retry字段,默认重试间隔 3s),可手动设置eventSource.retry = 1000调整间隔;若需自定义重连逻辑,可在onerror中关闭旧连接并创建新EventSource。 - 轮询:失败后根据退避策略延迟发送下一次请求;长轮询需注意:若服务器因超时断开,客户端应立即发起新请求(避免额外延迟)。
# 三、心跳设计(连接保活与有效性检测)
网络可能存在 “假连接”(如客户端断网但连接未显式断开),需通过心跳检测连接有效性:
# 1. 通用心跳逻辑
- 心跳包内容:简单的标识信息(如
{ type: 'ping' }),无需业务数据。 - 超时阈值:设定心跳间隔(如 30s)和超时时间(如 10s),超过阈值未收到响应则判定连接失效。
# 2. 技术适配细节
- WebSocket:
- 利用协议原生
ping/pong帧(比自定义消息更轻量):- 客户端定期发送
ping帧(ws.send(JSON.stringify({ type: 'ping' }))或调用ws.ping())。 - 服务器收到后回复
pong帧;客户端若 10s 内未收到pong,触发重连。
- 客户端定期发送
- 利用协议原生
- SSE:
- 服务器定期发送 “空消息” 作为心跳(如
data: heartbeat\n\n)。 - 客户端监听
message事件,记录最后一次接收时间;若超过 40s(30s 间隔 + 10s 超时)未收到任何消息(包括心跳),判定连接失效,关闭旧EventSource并重建。
- 服务器定期发送 “空消息” 作为心跳(如
- 轮询:
- 短轮询:将请求间隔作为心跳周期,若连续 3 次请求失败,判定连接失效,暂停轮询并触发重连。
- 长轮询:服务器在无业务数据时,定期返回 “心跳响应”(如
{ type: 'heartbeat' });客户端若超过超时时间未收到响应,立即发起新请求。
# 总结
- 优先选 WebSocket(双向实时)或 SSE(单向推送),轮询仅作为兼容降级方案。
- 重连核心:异常检测 + 指数退避 + 状态恢复;心跳核心:定期检测 + 超时判定。
- 实现时需结合业务场景调整间隔(如高频交互场景心跳间隔缩短至 10s),避免过度消耗资源。
# *16. DOM 操作的性能风险知道嘛?讲一下批量更新策略。
# 一、性能风险
DOM 操作的核心性能风险源于 重排(Reflow) 和 重绘(Repaint):
- 重排:当 DOM 元素的布局(位置、尺寸、结构)发生变化时,浏览器需重新计算元素几何信息并更新布局,消耗大量 CPU 资源(如修改
width、height、offsetTop等)。 - 重绘:元素样式(如颜色、背景)变化但不影响布局时,浏览器仅重新绘制元素外观,性能消耗比重排小,但频繁触发仍会卡顿。
- 风险点:频繁的 DOM 操作(如循环中修改样式、插入节点)会导致多次重排 / 重绘,阻塞主线程,造成页面卡顿、交互延迟。
# 二、批量更新策略
核心思路:减少重排 / 重绘次数,将多次分散的 DOM 操作合并为一次批量操作。
离线 DOM 操作(DocumentFragment)
原理:先在内存中创建
DocumentFragment(虚拟 DOM 容器),将所有待插入的 DOM 节点添加到片段中,最后一次性插入文档。效果:仅触发1 次重排(插入片段时),而非多次插入单个节点的多次重排。
示例:
const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const div = document.createElement('div'); fragment.appendChild(div); // 内存中操作,不触发重排 } document.body.appendChild(fragment); // 仅1次重排1
2
3
4
5
6
样式集中修改
合并样式操作:避免多次单独修改样式属性(如
elem.style.width = '100px'; elem.style.height = '100px'),改为一次性设置style或通过 CSS 类切换。示例:
// 优化前:2次可能的重排 elem.style.width = '100px'; elem.style.height = '100px'; // 优化后:1次重排 elem.style.cssText = 'width: 100px; height: 100px;'; // 或用类名:elem.classList.add('active');(CSS中定义所有样式)1
2
3
4
5
6
7
避免 “读写交错” 触发强制同步布局
风险:连续读取布局属性(如
offsetHeight、getBoundingClientRect)后立即修改样式,浏览器会强制触发重排以保证读取值的准确性,导致额外性能消耗。优化:先批量读取所有需要的布局属性,再批量修改样式。
示例:
// 优化前:读写交错,多次强制重排 for (let i = 0; i < 100; i++) { const height = elem.offsetHeight; // 读 elem.style.height = height + 10 + 'px'; // 写 → 强制重排 } // 优化后:先读后写,1次重排 const height = elem.offsetHeight; // 批量读 for (let i = 0; i < 100; i++) { elem.style.height = height + 10 + 'px'; // 批量写 }1
2
3
4
5
6
7
8
9
10
11
隐藏元素再操作
- 先将元素设置为
display: none(触发 1 次重排),在隐藏状态下完成所有 DOM 修改(无重排),最后恢复显示(再触发 1 次重排),总重排次数从多次减少到 2 次。
- 先将元素设置为
使用虚拟 DOM
- 框架(如 Vue、React)通过虚拟 DOM 先在内存中计算 DOM 变化,最终仅将差异批量更新到真实 DOM,大幅减少重排 / 重绘次数。
# 17. 从输入 URL 到页面渲染完整链路,请分阶段解释关键环节与可观测点。
整个过程可以分为以下几个关键阶段:
- 导航阶段 (Navigation)
- 用户输入与解析: 用户在地址栏输入 URL,浏览器会解析这个 URL,判断是搜索内容还是一个合法的网址。
- DNS 解析: 浏览器首先会查找各级缓存(浏览器缓存、操作系统缓存、路由器缓存、ISP 缓存),看是否有该域名对应的 IP 地址。如果都没有,则会向根域名服务器发起递归查询,最终获取到目标服务器的 IP 地址。
- 建立 TCP 连接: 浏览器利用 IP 地址和端口号,与服务器进行“三次握手”,建立一个可靠的 TCP 连接。
- TLS 握手: 如果是 HTTPS 协议,还需要在 TCP 连接之上进行 TLS/SSL 握手,协商加密密钥,建立安全的加密通道。
- 发送 HTTP 请求: 浏览器构建一个 HTTP 请求报文(包含请求行、请求头、请求体),通过建立好的连接发送给服务器。
- 可观测点: 浏览器开发者工具的Network面板可以清晰地看到这个阶段的耗时,包括DNS Lookup,Initial connection,SSL/TLS handshake以及Time to First Byte (TTFB)。TTFB是一个关键指标,它衡量了从请求发出到收到服务器第一个字节响应的时间。
- 响应与解析阶段 (Response & Parsing)
- 服务器处理与响应: 服务器接收到请求后,进行处理(查询数据库、执行业务逻辑等),然后返回一个 HTTP 响应报文(包含状态码、响应头、响应体)。响应体通常就是 HTML 文档。
- 解析 HTML 构建 DOM 树: 浏览器接收到 HTML 后,渲染引擎会自上而下逐行解析,生成一个树状结构的 DOM (Document Object Model) 对象。
- 解析 CSS 构建 CSSOM 树: 在解析过程中,如果遇到 CSS 链接或样式代码,会去加载并解析 CSS,生成 CSSOM (CSS Object Model) 树。CSS 的解析不会阻塞 DOM 的解析。
- JavaScript 的执行: 如果遇到
<script>标签,浏览器会暂停 HTML 的解析,转而去下载并执行 JavaScript。因为 JS 可能会修改 DOM 和 CSSOM,所以需要阻塞以保证后续解析的正确性。可以通过defer或async属性来改变这一行为。 - 可观测点: 开发者工具的Performance面板可以录制加载过程,观察到Parse HTML,Parse Stylesheet等事件。
- 渲染阶段 (Rendering)
- 构建渲染树 (Render Tree): 将 DOM 树和 CSSOM 树结合起来,生成渲染树。渲染树只包含需要显示在页面上的节点及其样式信息(例如display: none的节点不会出现在渲染树中)。
- 布局 (Layout / Reflow): 根据渲染树,计算出每个节点在屏幕上的精确位置和大小。这个过程也称为“回流”或“重排”。
- 绘制 (Paint / Repaint): 根据布局阶段计算出的信息,将每个节点绘制到屏幕上,包括文本、颜色、边框、阴影等。这个过程也称为“重绘”。
- 合成 (Compositing): 浏览器会将页面的不同部分(特别是涉及动画、transform等属性的元素)提升到独立的“层”中。当这些层发生变化时,浏览器只需重新绘制该层,然后将所有层合并(合成)到屏幕上,而无需对整个页面进行重排和重绘,极大地提高了性能。
- 可观测点: Performance面板中的Recalculate Style,Layout,Paint,Composite Layers事件详细记录了这一阶段的开销。频繁的Layout(回流) 是前端性能优化的重点关注对象。
# 18. Web App开发时如何实现对大屏幕的自适应?
**媒体查询(Media Queries)**基于屏幕宽度(如
min-width: 1200px)定义断点,针对大屏幕单独编写样式(如调整布局、字体大小、间距),示例:@media (min-width: 1440px) { .container { max-width: 1200px; padding: 0 40px; } }1弹性布局与网格布局
- 用
Flexbox(display: flex)实现元素弹性排列,通过flex-wrap、flex-grow适应屏幕宽度变化; - 用
Grid(display: grid)定义多列布局,结合grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))自动适配列数,大屏幕自动增加列数。
- 用
相对单位
- 使用
vw/vh(视口宽高的百分比)设置容器尺寸、字体大小(如font-size: 2vw),随屏幕宽度等比缩放; - 用
rem结合根元素字体大小(html { font-size: 62.5%; }),通过媒体查询动态调整根字体,间接控制元素尺寸。
- 使用
**限制最大宽度(max-width)**为核心容器设置
max-width(如1600px)并居中(margin: 0 auto),避免内容在超大屏幕上过度拉伸,保持可读性。响应式组件与布局重构大屏幕下拆分紧凑布局为多区域(如移动端单列 → 大屏幕双列 / 三列),通过条件渲染或样式切换调整组件排列(如侧边栏从折叠变为常驻)。
图片与资源适配用
srcset和sizes为图片提供多分辨率版本,大屏幕自动加载高清图:<img src="small.jpg" srcset="large.jpg 1200w" sizes="(min-width: 1200px) 1000px, 100vw">1
# *19. 首屏加载优化都有哪些方式?
- 资源压缩与合并
- 压缩 JS/CSS(Terser、CSSNano)、图片(WebP/AVIF 格式,OptiPNG 工具),减少文件体积;
- 合并零散资源(如小图片合并为雪碧图),减少 HTTP 请求数。
- 代码分割与懒加载
- 代码分割:通过
import()动态导入非首屏代码(如路由组件),仅加载当前页面必要逻辑; - 懒加载:图片用
loading="lazy"或 JS 实现延迟加载,组件用defineAsyncComponent(Vue)/React.lazy(React)按需加载。
- 代码分割:通过
- 缓存策略
- HTTP 缓存:设置
Cache-Control(强缓存)、ETag/Last-Modified(协商缓存),缓存静态资源; - 本地缓存:用
localStorage存储非敏感静态数据(如配置信息),减少重复请求; - Service Worker:缓存关键资源,支持离线访问。
- HTTP 缓存:设置
- CDN 与服务器优化
- 静态资源(JS/CSS/ 图片)部署到 CDN,利用就近节点加速传输;
- 启用 Gzip/Brotli 压缩,减少传输体积;升级 HTTP/2/3,多路复用降低连接开销。
- 首屏渲染加速
- 关键 CSS 内联(避免外部 CSS 阻塞渲染),非关键 CSS 异步加载;
- SSR(服务端渲染)或 SSG(静态站点生成):直接返回渲染好的 HTML,减少客户端渲染时间(如 Nuxt.js、Next.js);
- 预加载关键资源:用
<link rel="preload">提前加载首屏必需资源(如字体、核心 JS)。
- 代码层面优化
- 减少第三方库体积:按需引入(如 Lodash 用
lodash-es配合 tree-shaking),替换为轻量库; - 简化首屏 DOM 结构:减少嵌套层级和冗余节点,降低渲染引擎解析成本。
- 减少第三方库体积:按需引入(如 Lodash 用
# *20. 前端大数据量渲染性能如何优化?
- 虚拟列表(Virtual List)
- 核心:仅渲染可视区域内的 DOM 节点,非可视区域内容不渲染(通过计算滚动位置动态更新可视区数据)。
- 原理:监听滚动事件,计算当前可视区域的起始 / 结束索引,只渲染该范围内的数据,大幅减少 DOM 节点数量(如 10 万条数据仅渲染 20-30 条可视项)。
- 工具:使用成熟库(如
react-window、vue-virtual-scroller),避免重复开发。
- 分页加载与懒加载
- 分页:将数据按页拆分(如每页 20 条),通过分页器或滚动触底加载下一页,避免一次性加载全部数据。
- 懒加载:结合滚动事件,当数据区域即将进入可视区时再请求 / 渲染(如
IntersectionObserver监听元素可见性)。
- DOM 优化
- 减少 DOM 操作频率:用
DocumentFragment批量插入 DOM,避免频繁触发重排(Reflow)/ 重绘(Repaint)。 - 简化 DOM 结构:减少嵌套层级(如避免多层
div嵌套),删除冗余节点,降低渲染引擎解析成本。 - 避免复杂样式:减少
box-shadow、filter,必要时用will-change: transform或者transform: translateZ(0)触发硬件加速(适度使用)。
- 减少 DOM 操作频率:用
- 数据处理与缓存
- 数据预处理:在渲染前过滤、排序、格式化数据(复杂计算放 Web Worker,避免阻塞主线程)。
- 缓存复用:复用已渲染的 DOM 节点(如列表项),通过更新内容而非销毁重建(类似 React 的 Diff 算法);缓存计算结果(如
useMemo)避免重复计算。
- 避免同步渲染阻塞
- 分片渲染:将大数据量渲染拆分为多个小任务,用
requestIdleCallback或setTimeout分批执行,让出主线程给用户交互。 - 防抖 / 节流:数据频繁更新时(如搜索输入),用防抖(
debounce)控制渲染频率,避免高频触发重绘。
- 分片渲染:将大数据量渲染拆分为多个小任务,用
- 其他策略
- 用 Canvas/SVG 替代 DOM:极大量数据(如 10 万 + 点的图表)用 Canvas(如 ECharts)或 SVG 绘制,绕过 DOM 渲染瓶颈。
- 减少监听事件:列表项避免绑定过多事件,改用事件委托(父元素统一监听)。