前端面试题

2025/10/29 面试题Vue

# 前端面试题

# 1. 如果现在要从零搭建一个项目,你会怎么做?

从零搭建项目需遵循 “规划→搭建→开发→优化→上线” 的闭环流程,核心是先明确目标再落地,避免后期架构重构。以下是分阶段关键步骤,覆盖技术选型、环境配置、业务开发等核心环节。

# 一、前期规划:定方向、避返工

前期规划决定项目基础走向,需明确核心目标与工具选型,避免盲目开发。

  1. 明确项目核心信息

    • 项目类型:确认是 PC 端官网、移动端 H5、管理系统还是小程序。
    • 核心功能:列出必做功能(如用户登录、数据列表、表单提交)与可选功能。
    • 用户群体:若面向 C 端需考虑兼容性,面向 B 端(如管理系统)可优先保证功能完整性。
  2. 技术栈选型(按需匹配)

    技术类别 常见选项及适用场景
    前端框架 React(复杂交互,如电商)、Vue(轻量易上手,如官网)、Svelte(性能优先)
    构建工具 Vite(热更新快,中小型项目)、Webpack(配置灵活,大型项目)
    状态管理 Pinia(Vue 生态,轻量)、Redux Toolkit(React 生态,规范)
    路由 Vue Router(Vue 生态)、React Router(React 生态)
    UI 组件库 Ant Design(中后台)、Element Plus(Vue 生态)、Tailwind CSS(自定义样式)
  3. 设计项目目录结构提前规划src目录,保证代码可维护性,示例结构如下:

    src/
    ├─ components/  # 公共组件(如按钮、弹窗)
    ├─ pages/       # 页面组件(如首页、登录页)
    ├─ api/         # 接口封装(统一管理接口地址和请求)
    ├─ utils/       # 工具函数(如时间格式化、权限判断)
    ├─ router/      # 路由配置(路由规则、守卫)
    ├─ store/       # 状态管理(全局数据存储)
    └─ styles/      # 全局样式(如变量、重置样式)
    
    1
    2
    3
    4
    5
    6
    7
    8

# 二、环境搭建:搭基础、定规范

环境搭建是项目的 “地基”,需统一工具配置与开发规范,减少协作冲突。

  1. **初始化项目(用构建工具快速创建)**根据技术栈选择对应命令,生成基础框架:
    • Vue + Vite:npm create vite@latest 项目名 -- --template vue
    • React + Vite:npm create vite@latest 项目名 -- --template react
    • React + Create React App:npx create-react-app 项目名
  2. 配置开发规范(统一代码风格)
    • 代码检查:安装eslint(语法检查)和prettier(格式化),并创建配置文件(.eslintrc.js.prettierrc),示例规则:
      • ESLint:禁止未声明变量、强制使用分号。
      • Prettier:设置缩进 2 空格、单行最大长度 120 字符。
    • Git 提交规范:用husky + commitlint限制提交信息格式,如feat: 新增登录功能fix: 修复表单校验bug
  3. 引入核心依赖并配置安装项目必需依赖,并做基础配置:
    • 接口请求:安装axios,封装请求拦截(加 token)、响应拦截(统一错误处理)。
    • 路由:安装vue-router/react-router-dom,配置路由规则与登录守卫(未登录跳转登录页)。
    • 状态管理:安装pinia/@reduxjs/toolkit,初始化全局状态(如用户信息)。

# 三、核心开发:建骨架、填功能

开发阶段需先搭 “骨架” 再填业务,保证架构稳定后再做细节。

  1. 搭建基础架构(先实现核心能力)

    • 全局样式:引入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
  2. 开发业务模块(按优先级拆分)

    • 优先开发基础页面:如登录页、首页,实现页面跳转与基础布局。
    • 再开发复杂功能:如数据列表(分页、搜索)、表单提交(校验、提交 loading)、详情页(路由传参)。
    • 联调后端接口:调用api目录的接口,处理数据渲染(如列表渲染用v-for/map)与错误提示(如请求失败弹窗)。
  3. 适配与兼容性处理

    • 移动端:用postcss-pxtorem将 px 自动转为 rem,配合lib-flexible实现多屏幕适配。
    • 浏览器兼容:用@babel/preset-env+core-js处理 ES6 + 语法,保证在 IE11 等低版本浏览器正常运行。

# 四、优化与测试:提性能、保质量

上线前需做性能优化与测试,避免线上问题。

  1. 项目性能优化
    • 打包优化:Vite/Webpack 配置代码分割(splitChunks)、Tree Shaking(删除无用代码),减小包体积。
    • 加载优化:实现路由懒加载(如const Home = () => import('./pages/Home'))、图片懒加载(用vue-lazyload或原生loading="lazy")。
    • 体验优化:加页面加载动画、按钮点击反馈、表单实时校验提示。
  2. 测试验证(多维度检查)
    • 单元测试:用jest测试工具函数(如时间格式化函数)和基础组件(如按钮点击事件)。
    • 手动测试:检查功能完整性(登录、提交、跳转是否正常)、兼容性(Chrome/Firefox/Safari)、响应式(手机 / 平板 / PC)。

# 五、部署上线:推生产、做监控

部署是项目落地的最后一步,需确保线上稳定运行。

  1. 构建生产包执行打包命令生成dist文件夹(生产环境代码):

    • Vite:npm run build

    • Webpack:npm run build

      打包后检查dist目录大小,重点看vendor.js(第三方依赖)是否过大,必要时进一步优化。

  2. 选择部署平台(按需选择)

    • 静态项目(如官网、H5):部署到 Vercel(自动 CI/CD)、Netlify、阿里云 OSS(配合 CDN 加速)。
    • 需后端服务的项目:部署到阿里云 ECS、腾讯云服务器,配置 Nginx 反向代理(解决跨域)。
  3. 上线后监控

    • 错误监控:接入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新增了什么功能?

  1. 变量声明let(块级作用域,不可重复声明)、const(声明常量,块级作用域),替代 var 解决作用域问题。

  2. 箭头函数() => {} 简化函数写法,不绑定自身 this(继承外层上下文 this)。

  3. 解构赋值:快速提取数组 / 对象属性,如 const { a, b } = obj; const [x, y] = arr;

  4. 模板字符串:反引号包裹,支持多行文本和变量插入。

    `${变量}`
    
    1
  5. 类与继承class 语法糖(替代原型链),extends 实现继承,含 constructorsuper 等。

  6. 模块系统import 导入、export 导出模块,支持模块化开发。

  7. Promise:异步编程解决方案,避免回调地狱,支持 then/catch 链式调用。

  8. 新数据结构Set(无重复值集合)、Map(键值对集合,键可任意类型)。

  9. 函数增强:默认参数(function fn(a=1) {})、剩余参数(...args)、扩展运算符(... 展开数组 / 对象)。

  10. 其他for...of 循环(遍历可迭代对象)、Symbol(唯一值类型)、Proxy(对象代理)等。

# *4. 什么是Promise?

  • 定义:JavaScript 中用于处理异步操作的对象,代表一个异步操作的最终完成(或失败)及其结果值。

  • 核心特性

    • 三种状态pending(初始状态)、fulfilled(操作成功)、rejected(操作失败),状态一旦改变(从 pendingfulfilledrejected)则不可逆。
    • 链式调用:通过 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 整体代码、setTimeoutsetInterval、I/O 操作(如接口请求)、UI 渲染、setImmediate(Node 环境)。
    • 特点:优先级较低,执行间隔较长,每次事件循环仅执行一个宏任务
  • 微任务(Micro Task)

    • 包含:Promise.then/catch/finallyasync/await(本质是 Promise 语法糖)、queueMicrotaskprocess.nextTick(Node 环境,优先级高于 Promise)。
    • 特点:优先级高于宏任务,当前宏任务执行完毕后,会清空所有微任务队列再执行下一个宏任务。
  • 执行顺序

    1. 执行同步代码(属于当前宏任务);
    2. 同步代码执行完,检查并执行所有微任务(按入队顺序);
    3. 微任务执行完,执行 UI 渲染(若有);
    4. 从宏任务队列取一个任务执行,重复步骤 1-3(事件循环)。
  • 示例

    console.log('同步代码'); // 同步执行
    setTimeout(() => console.log('宏任务 setTimeout'), 0); // 宏任务队列
    Promise.resolve().then(() => console.log('微任务 then')); // 微任务队列
    // 输出顺序:同步代码 → 微任务 then → 宏任务 setTimeout
    
    1
    2
    3
    4

# 7. 如何判断数组?

  1. Array.isArray()
    • 用法:Array.isArray(arr)
    • 优点:ES5 新增的标准方法,专门用于判断数组,最可靠、简洁。
    • 缺点:不支持 IE8 及以下(可通过 polyfill 兼容)。
  2. Object.prototype.toString.call()
    • 用法:Object.prototype.toString.call(arr) === '[object Array]'
    • 优点:兼容性极好(支持所有浏览器),不受全局环境影响(如 iframe 中创建的数组也能正确判断)。
    • 缺点:写法稍繁琐。
  3. instanceof 操作符
    • 用法:arr instanceof Array
    • 优点:简单直观。
    • 缺点:若数组在不同全局环境(如 iframe)中创建,因 Array 构造函数不同,会导致判断失效(返回 false)。
  4. constructor 属性
    • 用法:arr.constructor === Array
    • 优点:写法简单。
    • 缺点:constructor 可被手动修改(如 arr.constructor = Object),导致判断不可靠,不推荐使用。

推荐方案:优先使用 Array.isArray()(现代环境),需兼容旧环境时用 Object.prototype.toString.call()

# 8. call、apply、bind的区别。

三者均用于改变函数执行时的 this 指向,核心区别在于参数传递、执行时机和返回值:

  1. 参数传递
    • call(thisArg, arg1, arg2, ...):接收多个参数,第一个为 this 指向的对象,后续为函数的参数列表(逗号分隔)。
    • apply(thisArg, [argsArray]):接收两个参数,第一个为 this 指向的对象,第二个为参数数组(或类数组对象,如 arguments)。
    • bind(thisArg, arg1, arg2, ...):参数格式同 call,支持分阶段传参(先传部分参数,后续调用新函数时补传剩余参数)。
  2. 执行时机
    • call/apply立即执行原函数。
    • bind不立即执行,返回一个绑定了 this 和部分参数的新函数,需手动调用新函数才执行。
  3. 返回值
    • 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)
1
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. 如何解决跨域?

跨域由浏览器同源策略(协议、域名、端口任一不同即跨域)导致,常见解决方式:

  1. CORS(跨域资源共享)

    • 原理:服务器端通过设置响应头(如 Access-Control-Allow-Origin: * 或指定域名)允许跨域请求。
    • 适用:前后端分离项目,需后端配合配置,支持所有 HTTP 方法,最推荐的现代方案。
  2. JSONP

    • 原理:利用 <script> 标签不受同源策略限制的特性,动态创建 script 标签,通过回调函数获取数据(仅支持 GET 请求)。
    • 适用:兼容性要求高的场景(如旧浏览器),但功能有限(仅 GET),已逐渐被 CORS 替代。
  3. 代理服务器

    • 原理:前端请求同源的代理服务器,由代理服务器转发请求到目标跨域服务器(服务器间无跨域限制)。

    • 场景:开发环境(如 Webpack DevServer 配置 proxy)、生产环境(Nginx 反向代理)。

    • 示例(Webpack):

      devServer: { proxy: { '/api': { target: 'https://跨域域名', changeOrigin: true } } }
      
      1
  4. postMessage

    • 原理:用于不同域名的页面(如 iframe 嵌套)间通信,通过 window.postMessage 发送数据,监听 message 事件接收。
    • 适用:页面间跨域通信(如父页面与子 iframe 交互)。
  5. WebSocket

    • 原理:WebSocket 协议(ws:///wss://)本身不限制跨域,建立连接后可双向通信。
    • 适用:实时通信场景(如聊天、实时数据更新)。

优先推荐:CORS(简单高效)、代理服务器(开发环境便捷)。

# *11. TypeScript函数的参数类型都有哪些?

在 TypeScript 中,函数参数的类型可以是任意合法的 TypeScript 类型,核心可分为以下几类,涵盖基础类型、复合类型、特殊场景类型等:

# 1. 基础类型参数

即 TypeScript 支持的原始类型(numberstringboolean 等),直接约束参数的基础数据类型。

// 数字类型参数
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";
}
1
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 }); // 正确
1
2
3
4

# (2)接口 / 类型别名定义的对象

通过 interfacetype 复用对象类型:

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);
}
1
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]}`;
}
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"
1
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)
1
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"
1
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 参数)
1
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"]
1
2
3
4
5
6
7
8
9

# 8. 泛型参数

参数类型为泛型(TU 等),可动态适配不同类型,保持输入输出类型一致。

// 泛型参数 T:输入和返回值类型相同
function identity<T>(arg: T): T {
  return arg;
}
identity("hello"); // 类型:string(自动推断 T 为 string)
identity<number>(123); // 类型:number(显式指定 T)
1
2
3
4
5
6

# 9. 其他特殊类型参数

  • null/undefined:参数只能是 nullundefined(需开启 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
    5
  • never 类型:参数类型为 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
    11
  • interface(接口):仅用于描述对象类型(包括对象、数组、函数对象等),无法为基本类型、联合类型等创建接口。

    // 描述对象
    interface User {
      name: string;
      age: number;
    }
    
    // 描述函数对象(函数的结构)
    interface Multiply {
      (a: number, b: number): number;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

# 2. 扩展方式:interfaceextendstype 用交叉类型(&

两者都支持扩展,但语法和行为不同:

  • interface 扩展:通过 extends 关键字,可扩展另一个 interfacetype(对象类型)。

    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
    18
  • type 扩展:通过交叉类型(&)合并多个类型,生成新类型。

    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
    12
  • type 重复声明会报错:类型别名不可重复定义,否则会触发类型冲突。

    type Name = string;
    type Name = number; // 报错:标识符“Name”重复
    
    1
    2

# 4. 实现(implements):class 对两者的支持有差异

class 可以通过 implements 实现 interfacetype(对象类型),但 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 }
1
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)
1
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)
1
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(自动推断函数返回值)
1
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
1
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")
1
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
1
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 }
1
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 }
1
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 }
1
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"; // 错误(不匹配前缀)
1
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; ... }
1
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 } // 支持嵌套对象
};
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)
}
1
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 识别扩展的方法)
1
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 协议 极高 持久连接,双向低延迟,开销小 实现稍复杂,需处理握手 / 断开逻辑 双向实时交互(如聊天、在线协作、游戏)

选型原则

  1. 通信方向:双向交互必选 WebSocket;仅服务器推选用 SSE(更轻量)。
  2. 实时性要求:毫秒级实时选 WebSocket;秒级延迟可选 SSE / 长轮询;非实时选短轮询。
  3. 兼容性:若需兼容极旧浏览器(如 IE8-),退选轮询;现代浏览器优先 WebSocket/SSE。
  4. 服务器压力:高并发场景下,WebSocket(持久连接)比轮询(频繁建连)更优;SSE 比长轮询更轻量。

# 二、重连设计(通用逻辑 + 技术适配)

当连接异常断开(如网络波动、服务器重启),需自动重连以恢复通信,核心设计如下:

# 1. 通用重连逻辑
  • 断开检测
    • WebSocket:监听 onclose 事件(event.code 非 1000/1001 表示异常断开)。
    • SSE:监听 onerror 事件(或 readyState 变为 CLOSED)。
    • 轮询:连续多次请求失败(如 HTTP 503 / 超时)。
  • 重试策略
    • 指数退避:避免频繁重试压垮服务器,设置一个初始重连延迟(如 1s),如果失败,下一次延迟翻倍(2s, 4s, 8s...),并设置一个最大延迟上限(如 60s)。
    • 随机抖动:在计算出的延迟上增加一个小的随机值,防止所有掉线的客户端在同一时刻“风暴般”地重连服务器。
    • 最大重试次数:设置阈值(如 10 次),超过后提示用户手动重连(避免无限重试)。
    • 网络恢复检测:通过 navigator.onLine 监听客户端网络状态,网络恢复后立即重试。
  • 状态保存:重连期间缓存客户端待发送的消息(如 WebSocket 的未发送数据),重连成功后补发。
# 2. 技术适配细节
  • WebSocket:断开后调用 new WebSocket(url) 重建连接,需重新触发握手流程;重连成功后需重新订阅事件(如服务器端的主题订阅)。
  • SSE:浏览器原生支持自动重连(通过 EventSourceretry 字段,默认重试间隔 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 资源(如修改 widthheightoffsetTop 等)。
  • 重绘:元素样式(如颜色、背景)变化但不影响布局时,浏览器仅重新绘制元素外观,性能消耗比重排小,但频繁触发仍会卡顿。
  • 风险点:频繁的 DOM 操作(如循环中修改样式、插入节点)会导致多次重排 / 重绘,阻塞主线程,造成页面卡顿、交互延迟。

# 二、批量更新策略

核心思路:减少重排 / 重绘次数,将多次分散的 DOM 操作合并为一次批量操作

  1. 离线 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
  2. 样式集中修改

    • 合并样式操作:避免多次单独修改样式属性(如 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
  3. 避免 “读写交错” 触发强制同步布局

    • 风险:连续读取布局属性(如 offsetHeightgetBoundingClientRect)后立即修改样式,浏览器会强制触发重排以保证读取值的准确性,导致额外性能消耗。

    • 优化:先批量读取所有需要的布局属性,再批量修改样式。

    • 示例:

      // 优化前:读写交错,多次强制重排
      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
  4. 隐藏元素再操作

    • 先将元素设置为 display: none(触发 1 次重排),在隐藏状态下完成所有 DOM 修改(无重排),最后恢复显示(再触发 1 次重排),总重排次数从多次减少到 2 次。
  5. 使用虚拟 DOM

    • 框架(如 Vue、React)通过虚拟 DOM 先在内存中计算 DOM 变化,最终仅将差异批量更新到真实 DOM,大幅减少重排 / 重绘次数。

# 17. 从输入 URL 到页面渲染完整链路,请分阶段解释关键环节与可观测点。

整个过程可以分为以下几个关键阶段:

  1. 导航阶段 (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是一个关键指标,它衡量了从请求发出到收到服务器第一个字节响应的时间。
  2. 响应与解析阶段 (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等事件。
  3. 渲染阶段 (Rendering)
    • 构建渲染树 (Render Tree): 将 DOM 树和 CSSOM 树结合起来,生成渲染树。渲染树只包含需要显示在页面上的节点及其样式信息(例如display: none的节点不会出现在渲染树中)。
    • 布局 (Layout / Reflow): 根据渲染树,计算出每个节点在屏幕上的精确位置和大小。这个过程也称为“回流”或“重排”。
    • 绘制 (Paint / Repaint): 根据布局阶段计算出的信息,将每个节点绘制到屏幕上,包括文本、颜色、边框、阴影等。这个过程也称为“重绘”。
    • 合成 (Compositing): 浏览器会将页面的不同部分(特别是涉及动画、transform等属性的元素)提升到独立的“层”中。当这些层发生变化时,浏览器只需重新绘制该层,然后将所有层合并(合成)到屏幕上,而无需对整个页面进行重排和重绘,极大地提高了性能。
    • 可观测点: Performance面板中的Recalculate Style,Layout,Paint,Composite Layers事件详细记录了这一阶段的开销。频繁的Layout(回流) 是前端性能优化的重点关注对象。

# 18. Web App开发时如何实现对大屏幕的自适应?

  1. **媒体查询(Media Queries)**基于屏幕宽度(如 min-width: 1200px)定义断点,针对大屏幕单独编写样式(如调整布局、字体大小、间距),示例:

    @media (min-width: 1440px) { .container { max-width: 1200px; padding: 0 40px; } }
    
    1
  2. 弹性布局与网格布局

    • Flexboxdisplay: flex)实现元素弹性排列,通过 flex-wrapflex-grow 适应屏幕宽度变化;
    • Griddisplay: grid)定义多列布局,结合 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) 自动适配列数,大屏幕自动增加列数。
  3. 相对单位

    • 使用 vw/vh(视口宽高的百分比)设置容器尺寸、字体大小(如 font-size: 2vw),随屏幕宽度等比缩放;
    • rem 结合根元素字体大小(html { font-size: 62.5%; }),通过媒体查询动态调整根字体,间接控制元素尺寸。
  4. **限制最大宽度(max-width)**为核心容器设置 max-width(如 1600px)并居中(margin: 0 auto),避免内容在超大屏幕上过度拉伸,保持可读性。

  5. 响应式组件与布局重构大屏幕下拆分紧凑布局为多区域(如移动端单列 → 大屏幕双列 / 三列),通过条件渲染或样式切换调整组件排列(如侧边栏从折叠变为常驻)。

  6. 图片与资源适配srcsetsizes 为图片提供多分辨率版本,大屏幕自动加载高清图:

    <img src="small.jpg" srcset="large.jpg 1200w" sizes="(min-width: 1200px) 1000px, 100vw">
    
    1

# *19. 首屏加载优化都有哪些方式?

  1. 资源压缩与合并
    • 压缩 JS/CSS(Terser、CSSNano)、图片(WebP/AVIF 格式,OptiPNG 工具),减少文件体积;
    • 合并零散资源(如小图片合并为雪碧图),减少 HTTP 请求数。
  2. 代码分割与懒加载
    • 代码分割:通过 import() 动态导入非首屏代码(如路由组件),仅加载当前页面必要逻辑;
    • 懒加载:图片用 loading="lazy" 或 JS 实现延迟加载,组件用 defineAsyncComponent(Vue)/React.lazy(React)按需加载。
  3. 缓存策略
    • HTTP 缓存:设置 Cache-Control(强缓存)、ETag/Last-Modified(协商缓存),缓存静态资源;
    • 本地缓存:用 localStorage 存储非敏感静态数据(如配置信息),减少重复请求;
    • Service Worker:缓存关键资源,支持离线访问。
  4. CDN 与服务器优化
    • 静态资源(JS/CSS/ 图片)部署到 CDN,利用就近节点加速传输;
    • 启用 Gzip/Brotli 压缩,减少传输体积;升级 HTTP/2/3,多路复用降低连接开销。
  5. 首屏渲染加速
    • 关键 CSS 内联(避免外部 CSS 阻塞渲染),非关键 CSS 异步加载;
    • SSR(服务端渲染)或 SSG(静态站点生成):直接返回渲染好的 HTML,减少客户端渲染时间(如 Nuxt.js、Next.js);
    • 预加载关键资源:用 <link rel="preload"> 提前加载首屏必需资源(如字体、核心 JS)。
  6. 代码层面优化
    • 减少第三方库体积:按需引入(如 Lodash 用 lodash-es 配合 tree-shaking),替换为轻量库;
    • 简化首屏 DOM 结构:减少嵌套层级和冗余节点,降低渲染引擎解析成本。

# *20. 前端大数据量渲染性能如何优化?

  1. 虚拟列表(Virtual List)
    • 核心:仅渲染可视区域内的 DOM 节点,非可视区域内容不渲染(通过计算滚动位置动态更新可视区数据)。
    • 原理:监听滚动事件,计算当前可视区域的起始 / 结束索引,只渲染该范围内的数据,大幅减少 DOM 节点数量(如 10 万条数据仅渲染 20-30 条可视项)。
    • 工具:使用成熟库(如 react-windowvue-virtual-scroller),避免重复开发。
  2. 分页加载与懒加载
    • 分页:将数据按页拆分(如每页 20 条),通过分页器或滚动触底加载下一页,避免一次性加载全部数据。
    • 懒加载:结合滚动事件,当数据区域即将进入可视区时再请求 / 渲染(如 IntersectionObserver 监听元素可见性)。
  3. DOM 优化
    • 减少 DOM 操作频率:用 DocumentFragment 批量插入 DOM,避免频繁触发重排(Reflow)/ 重绘(Repaint)。
    • 简化 DOM 结构:减少嵌套层级(如避免多层 div 嵌套),删除冗余节点,降低渲染引擎解析成本。
    • 避免复杂样式:减少 box-shadowfilter,必要时用 will-change: transform或者transform: translateZ(0) 触发硬件加速(适度使用)。
  4. 数据处理与缓存
    • 数据预处理:在渲染前过滤、排序、格式化数据(复杂计算放 Web Worker,避免阻塞主线程)。
    • 缓存复用:复用已渲染的 DOM 节点(如列表项),通过更新内容而非销毁重建(类似 React 的 Diff 算法);缓存计算结果(如 useMemo)避免重复计算。
  5. 避免同步渲染阻塞
    • 分片渲染:将大数据量渲染拆分为多个小任务,用 requestIdleCallbacksetTimeout 分批执行,让出主线程给用户交互。
    • 防抖 / 节流:数据频繁更新时(如搜索输入),用防抖(debounce)控制渲染频率,避免高频触发重绘。
  6. 其他策略
    • 用 Canvas/SVG 替代 DOM:极大量数据(如 10 万 + 点的图表)用 Canvas(如 ECharts)或 SVG 绘制,绕过 DOM 渲染瓶颈。
    • 减少监听事件:列表项避免绑定过多事件,改用事件委托(父元素统一监听)。
Last Updated: 2025/10/30 10:01:35