React
useState 是如何实现的?
- 基于闭包:在函数组件外部维护状态存储。
- 基于链表:通过一个稳定的调用顺序,将多个 Hook 与它们各自的状态正确关联。
- 触发更新:
setState会更新状态并通知 React 调度一次新的渲染。 - 渲染重置:每次组件渲染时,内部指针都会重置,从而按顺序“消费”Hook。
setState 的实现原理和处理同步、异步
setState 的核心原理可以概括为:它不是立即同步更新状态的,而是将一个更新请求放入一个队列中,然后在一定时机下进行批量合并和更新,最后触发组件的重新渲染。
- 开启一个批量更新事务: 当 React 开始执行一个事件处理函数时,它会先“打开”一个批量更新的开关(
batchingUpdates)。 - 入队: 每次调用
setState,都不是直接修改this.state,而是将这次更新(包含新的状态值或更新函数)放入一个 待处理的更新队列(pending queue) 中。 - 合并: 在同一个事件循环中,如果多次调用
setState,React 会将这些更新合并(Object.assign)成一个更新。 - 出队与更新: 当事件处理函数执行完毕后,React 会“关闭”批量更新开关,然后一次性处理队列中的所有更新,计算出最终的新状态,并只进行一次重新渲染。
| 场景 | 表现 | 原理 |
|---|---|---|
| React 事件、生命周期 | 异步、批量 | React 通过“批量更新事务”机制,将更新收集到队列中,最后统一处理。 |
| setTimeout、Promise、原生事件 | 同步 | 脱离了 React 的事务控制,每次 setState 都会立即触发更新。 |
为什么 usestate 返回的是数组而不是对象?
- 自由命名(最主要的原因)
// ✅ 数组 - 可以自由命名
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
// ❌ 如果是对象 - 必须使用固定的属性名
const {state: count, setState: setCount} = useState(0);
const {state: name, setState: setName} = useState('');
- 代码简洁性
- 易于组合和自定义 Hook
- 一致性: 符合 React 模式
如何让 useEffect 支持 async/await?
- 在内部定义一个异步函数
- 在内部定义一个匿名的 IIFE 函数
- 使用第三方 Hook:
import React, {useState} from 'react';
import useAsyncEffect from 'use-async-effect';
const MyComponent = () => {useAsyncEffect(async () => {const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
}, []); // 依赖数组
return <div>{/* ... */}</div>;
};
react 中懒加载的实现原理是什么?
动态导入(Dynamic Import)
// 静态导入(打包时会包含整个模块)import MyComponent from './MyComponent';
// 动态导入(返回一个 Promise)import('./MyComponent').then(module => {
const MyComponent = module.default;
// 使用组件
});
React.lazy 的实现原理
const LazyComponent = React.lazy(() => import('./MyComponent'));
// 简化版的 React.lazy 实现:function lazy(loader) {
let component = null;
let status = 'pending'; // 'pending', 'success', 'error'
let result = null;
return function LazyComponent(props) {
// 首次加载
if (status === 'pending') {result = loader();
result.then(module => {
status = 'success';
component = module.default; // 获取默认导出
},
error => {
status = 'error';
result = error;
}
);
}
// 处理不同状态
if (status === 'pending') {throw result; // 抛出 Promise,Suspense 会捕获} else if (status === 'error') {throw result; // 抛出错误,Error Boundary 会捕获} else if (status === 'success') {return React.createElement(component, props);
}
};
}
Suspense 的工作原理:Suspense 组件通过 捕获子组件抛出的 Promise 来实现加载状态的管理
function App() {
return (<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
React 中,fiber 是如何实现时间切片的?
Fiber 实现时间切片的核心是:将渲染任务分解成小的工作单元,在每个工作单元之间检查是否应该让出主线程给更高优先级的任务。
- 可中断的循环机制 :Fiber 不再使用递归遍历,而是改用 循环遍历,通过
while循环处理每个 Fiber 节点,在循环中检查是否应该中断 - 让出控制权的检查
- 是否超时:判断当前时间片(通常 5ms)是否用完
- 是否有更高优先级任务:如用户输入、动画等需要立即响应
- 基于优先级的调度
- 同步优先级:不可中断(如输入框输入)
- 高优先级:用户交互、动画
- 普通优先级:数据更新
- 低优先级:非紧急任务
- 增量式单元工作
- 处理一个节点后检查时间
- 保存当前进度(workInProgress 指针)
- 下次从断点继续执行
Vue
Vue 中 Treeshaking 特性对比
| 特性维度 | Vue 2 🟢 | Vue 3 🔷 |
|---|---|---|
| 模块化设计 | 全局 API,整体打包 | 基于 ES Module 的模块化设计,API 可独立引入 |
| 打包结果 | 即使只使用部分功能,也需引入整个 Vue 库 | 仅打包你实际导入并使用的 API |
| 体积控制 | 难以优化,包体积相对固定 | 精细可控,有效减小最终打包大小 |
在 Vue 3 中,你可以只引入需要的 nextTick 函数,未使用的 API 将被移除。
import {nextTick} from 'vue'; // 按需引入特定 API
nextTick(() => { // 直接使用
// 一些和 DOM 有关的东西
});
vue3 相比较于 vue2,在编译阶段有哪些改进?
说说 Vue 页面渲染流程
- 模板 -(编译)
- 解析 :将模板字符串解析成一个 抽象语法树。
- 转换 :对 AST 进行优化和处理,例如 Vue 3 的 静态提升 和Patch Flag标记。
- 静态节点提升:识别出永远不会改变的静态节点,次重渲染时就不再重新创建它们,极大优化了性能。
- 指令处理:解析和处理 Vue 的指令。例如,将
v-if/v-for转换为对应的条件渲染和循环渲染逻辑。 - 表达式解析:将文本中的 Mustache 插值(如
{{message}})解析为 JavaScript 表达式(如_s(message))。
- 生成 :将优化后的 AST 转换为一个字符串,该字符串是渲染函数的代码。最终,这个字符串通过
new Function()变成一个可执行的 渲染函数。
- 渲染函数 -(执行)
- 虚拟 DOM -(比对)
- 通过 Diff 算法 (深度优先),计算出最小量的变更,将这些变更 应用到真实 DOM 上。
- 真实 DOM
文字描述 vue 是如何识别和解析指令的?
- 编译时:模板解析编译成 AST,遍历 AST 树遇到一个属性是以
v-开头(或@,:等简写),它就会将这个属性 识别为一个指令。转换 / 优化对于不同类型的指令,编译器会进行不同的处理。 - 运行时 :执行与响应,渲染:执行
render函数,生成带指令信息的 VNode。打补丁:对比新旧 VNode,调用指令的 钩子函数 。 更新:在钩子函数中操作真实 DOM,实现指令的最终效果。
Vue 有了数据响应式,为何还要 diff?
- 数据响应式 是 “侦察兵”,负责发现 哪里发生了改变 ,并通知到具体的 部队(组件)。
- Diff 算法 是 “作战参谋”(知道更新的颗粒度更细),负责为接到通知的部队制定 最高效、最精准的作战方案(如何更新 DOM)。
vue3 的 ref 为什么要使用.value 方式获取,而 template 模板不需要
为什么需要 .value?
- 对于 基本类型(如字符串、数字),在 JavaScript 中,它们是按值传递的,无法直接附加追踪逻辑。
ref通过将其包装成对象,使得 Vue 能够利用get和set拦截(对于对象类型,ref内部会使用reactive处理)来管理响应式。 - 明确标识响应式变量:在脚本中使用
.value,可以让你清晰地知道这是一个需要 Vue 追踪其变化的响应式变量,有助于代码的维护和阅读
模板中的自动 ” 解包 ”
- 编译时的魔法 :当 Vue 编译器处理模板时,如果它识别到某个变量是一个
ref,它会 自动帮你加上.value来访问实际数据。你写的{{count}}在编译后实际上相当于{{count.value}}。 - 保持模板简洁:这样做的目的是让模板保持简洁易读,毕竟在模板中频繁地写
.value会比较繁琐
Vue2 和 Vue3 的 Diff 区别
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 算法类型 | 双端比较 | 基于最长递增子序列的双端比较 |
| 比较方式 | 头头、尾尾、头尾、尾头、key 映射 | 头头、尾尾、最长递增子序列 |
| 移动优化 | 相对简单 | 使用 LIS 最小化 DOM 移动 |
| 静态标记 | 无 | 有 PatchFlags 静态标记 |
| 块优化 | 无 | 有 Block Tree 概念 |
| 性能 | O(n) 但移动较多 | O(n) 移动更少 |
Vue3.0 的设计目标是什么? 做了哪些优化?
说说 Vue 中 css scoped 的原理
scoped 的实现原理可以概括为:通过给组件模板中的 HTML 元素和样式选择器添加唯一的属性标识,然后利用 CSS 属性选择器来确保样式只匹配到当前组件的元素。
React 和 Vue 在技术层面有哪些区别?
| 特性 | React | Vue |
|---|---|---|
| 本质 | 库,专注于视图层 | 渐进式框架,提供完整解决方案 |
| 视图语法 | JSX (JavaScript in XML) | 基于 HTML 的模板 |
| 数据响应式 | 手动触发(setState/useState),单向数据流 |
自动追踪(Proxy/defineProperty),可变状态 |
| 逻辑组合 | Hooks (函数组件) | Options API & Composition API |
| 样式方案 | 无官方方案,社区多样(CSS-in-JS 等) | 单文件组件,原生支持 Scoped CSS |
| 类型支持 | 与 TypeScript 集成良好 | Vue 3 用 TS 重写,对 TS 支持极佳 |
| 性能瓶颈 | 大数据量的响应式追踪 | 大型组件树的递归渲染 |
| 学习曲线 | 较陡峭,需理解更多概念 | 较平缓,文档友好,易于上手 |
| 哲学 | 给予开发者自由,信仰 JavaScript | 提供结构和便利,降低开发门槛 |
Vue 的 keep-alive 原理是什么,在 react 是怎么实现同样效果的
keep-alive 的核心目标是 缓存组件实例,避免组件切换时的重复销毁和创建。在组件不活跃时保留其状态,在需要时重新激活。
- keep-alive 被标记为 抽象组件(abstract: true),不会出现在父子组件链中,不参与正常的 DOM 渲染,只负责管理子组件的缓存逻辑,自身不渲染任何内容
- 初始化阶段
- created 钩子:初始化 cache 对象和 keys 数组
- mounted 钩子:监听 include/exclude 参数变化,动态调整缓存
- 组件首次渲染 → 检查缓存资格 → 生成缓存 key → 存入 cache
- 检查组件名称 → 匹配 include/exclude 规则 → 判断是否需要缓存
- 组件再次渲染 → 检查缓存 key → 命中缓存 → 复用实例 → 激活组件
- 检查 cache[key]是否存在?
- 存在(缓存命中):- 从 cache 中取出缓存的 VNode-> 更新 keys 队列(LRU 移动), 标记 VNode.data.keepAlive = true
- 组件切换隐藏 → 停用组件 → 保持实例在缓存中
- 缓存数量超限 → LRU 淘汰 → 销毁最久未使用的实例
- 参数变化监听 → 动态调整缓存内容 → 清理不符合条件的缓存项
| 特性 | Vue keep-alive | React 实现方案 |
|---|---|---|
| 内置支持 | ✅ 官方内置 | ❌ 需要自行实现或使用第三方库(react-activation) |
| 生命周期 | ✅ activated/deactivated | ⚠️ 需要手动处理 |
| 缓存策略 | ✅ LRU 算法 | ⚠️ 需要手动实现 |
| TypeScript | ✅ 完整支持 | ✅ 良好支持 |
| 使用复杂度 | ✅ 简单易用 | ⚠️ 相对复杂 |
| 性能优化 | ✅ 自动优化 | ⚠️ 需要手动优化 |
Webpack
webpackloader 和 plugin 实现原理
| 特性 | Loader | Plugin |
|---|---|---|
| 作用对象 | 单个文件 | 整个构建过程 |
| 执行时机 | 文件加载阶段 | 编译的各个生命周期 |
| 功能 | 文件转换 | 更广泛的任务 |
| 使用方式 | 配置在 module.rules 中 | 配置在 plugins 数组中 |
前端工程化的理解
前端工程化是 将软件工程的方法和原则系统性地应用到前端开发中,通过工具、流程、规范的结合,提升开发效率、保证代码质量、优化项目维护性的系统性工程。
webpack 的 module、bundle、chunk 分别指的是什么?
- module: 在 Webpack 的世界里,一切皆模块 。一个模块就是你项目中的一个源代码文件,它可以通过
import或require语句引入其他模块。 - Chunk 是 Webpack 在打包过程中的一个 中间概念。它是多个 Module 的集合,是 Webpack 根据特定规则(如代码分割)进行分组后形成的。
- Bundle 是 Chunk 经过一系列处理(如压缩、混淆)和转换后,最终写入输出目录(output)的物理文件。它是用户最终在浏览器中加载的文件。
treeshaking 机制的原理是什么?
是一个用于移除 JavaScript 上下文中未引用代码(dead code)的优化技术。
- 静态分析:利用 ES6 模块的静态结构,在构建时分析出完整的依赖关系。
- ES6 模块(
import/export)是静态的 :模块的依赖关系在代码 运行前 就可以通过静态分析确定。 - CommonJS(
require/module.exports)是动态的 :依赖关系在 运行时 才能确定,无法在构建阶段可靠地分析。
- ES6 模块(
- 标记追踪:从入口开始,标记所有被使用的导出。
- 安全移除:在压缩阶段(主要由 Terser 完成),将未被标记的代码安全地从最终 Bundle 中删除。
- 深度优化:通过
package.json的sideEffects属性,实现更激进的 ” 模块级 ” 摇树,跳过整个未使用的纯模块。
如何提高 webpack 的构建速度?
与 webpack 类似的工具还有哪些? 区别?
webpack-dev-server 的原理
| 部件 | 职责 | 说明 |
|---|---|---|
| Express 静态资源服务器 | 提供静态文件访问能力。 | 负责响应浏览器对打包后资源的 HTTP 请求。 |
| WebSocket 连接 | 在服务器和浏览器之间建立双向通信通道。 | 用于服务器主动向浏览器推送通知,如编译完成、需要热更新等。 |
| webpack-dev-middleware | Webpack 和 Express 服务器之间的桥梁。 | 这是 最核心 的部件,负责以 监听模式 启动 Webpack 编译器,并将编译结果输出到 内存 而非文件系统。 |
说下 Vite 的原理
说说 webpack proxy 工作原理? 为什么能解决跨域?
作为一个 反向代理,拦截前端发出的特定请求,并将其透明地转发到真正的后端服务器。
将“浏览器 <-> 后端 API”的跨域请求,拆分成两段“浏览器 <-> 代理服务器”(同源)和“代理服务器 <-> 后端 API”(服务器间通信),从而绕过了浏览器的同源策略限制。
TypeScript
TypeScript 的主要特点是什么?
- 静态类型系统
- 类型推断
- JavaScript 超集:所有合法的 JavaScript 代码都是合法的 TypeScript 代码
- 接口和泛型
- ECMAScript 特性支持:支持最新的 ES6+ 特性、可编译为不同版本的 JavaScript
- 编译时检查:在编译阶段捕获类型错误、不会影响运行时性能
什么是 TypeScript Declare 关键字?
TypeScript Declare 关键字用于 声明 一个已经存在的变量、函数、类、枚举、模块或其他类型,而不需要提供具体的实现。
- 声明全局变量:仅声明不实现 – 只提供类型信息,不包含具体实现
- 声明第三方库
- 声明命名空间
- 编译时使用 – 在编译阶段被移除,不影响运行时
- 类型安全 – 为现有 JavaScript 代码提供 TypeScript 类型支持
- 环境声明 – 通常放在
.d.ts声明文件中
// 声明全局变量
declare var MY_GLOBAL: string;
// 声明全局常量
declare const APP_VERSION: string;
// 声明全局函数
declare function myGlobalFunction(name: string): void;
// 声明第三方库
declare module 'my-library' {export function doSomething(): void;
export const version: string;
}
// 使用
import {doSomething} from 'my-library';
doSomething();
// 环境变量声明 environment.d.ts
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
readonly API_KEY: string;
readonly DATABASE_URL: string;
}
}
// 使用
const apiKey = process.env.API_KEY;
Typescript 中的 getter/setter 是什么? 你如何使用它们?
Getter 和 Setter 是特殊的类方法,用于控制对类属性的访问和修改。它们允许你在属性访问时添加额外的逻辑。
class BankAccount {
private _balance: number = 0;
get balance(): number {return this._balance;}
set balance(amount: number) {if (amount < 0) {throw new Error('余额不能为负数');
}
this._balance = amount;
}
}
unknown 是什么类型?
unknown 是 TypeScript 3.0 引入的顶级类型,它表示 类型安全的 any。与 any (直接跳过类型安全校验)不同,unknown 要求在使用前进行类型检查或类型断言。
never 是什么类型,详细讲一下
never 类型表示 永远不会出现的值 的类型。它是 TypeScript 类型系统中最底层的类型,是所有类型的子类型(个人感觉有点像 null)。
// never 类型表示永远不会有返回值的函数
function throwError(message: string): never {throw new Error(message);
}
// 这些赋值都会报错
let a: never = 123; // Error
let b: never = "hello"; // Error
let c: never = null; // Error
let d: never = undefined; // Error
// 唯一能赋值给 never 的只有 never 本身
let e: never = throwError("error"); // OK
// void - 函数正常执行完成,但没有返回值
function logMessage(message: string): void {console.log(message);
// 隐式返回 undefined
}
let value: unknown;
value = 42; // ✅
value = "hello"; // ✅
value = true; // ✅
// 不能直接使用 unknown 类型的值
// value.toFixed(2); // ❌ 错误:Object is of type 'unknown'
// 需要先进行类型检查
if (typeof value === "number") {value.toFixed(2); // ✅ 现在 TypeScript 知道 value 是 number
}
// 或者使用类型断言
(value as number).toFixed(2); // ✅ 但需要开发者自己确保类型正确
// never - 函数永远不会正常完成
function crashProgram(): never {throw new Error("程序崩溃");
// 永远不会返回
}
// void、never 使用区别
let voidResult: void = logMessage("hello"); // OK
// let neverResult: never = logMessage("hello"); // Error
let neverResult: never = crashProgram(); // OK
// let voidResult: void = crashProgram(); // Error
- 表示不可能的情况 – 函数永远不会返回
- 穷尽性检查 – 确保处理所有可能的情况
- 类型运算 – 在条件类型中过滤和转换类型
- 防御性编程 – 捕获未处理的情况
TS 中 any、unknown、never、void 的区别
| 类型 | 可赋任何值 | 可赋给其他类型 | 可直接使用 | 类型安全 |
|---|---|---|---|---|
any |
✅ | ✅ | ✅ | ❌ |
unknown |
✅ | ❌ | ❌ | ✅ |
never |
❌ | ✅ | ❌ | ✅ |
void |
✅ | ✅ | ✅ | ✅ |
typescript 中的 is 关键字有什么用?
在 TypeScript 中,is 关键字用于 类型谓词(Type Predicates),它允许你在自定义函数中执行运行时检查,并帮助 TypeScript 编译器缩小变量的类型范围。
function isType(value: any): value is TargetType {// 返回布尔值}
interface Cat {meow(): void;
}
interface Dog {bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {return (animal as Cat).meow !== undefined;
}
function handleAnimal(animal: Cat | Dog) {if (isCat(animal)) {
// TypeScript 知道这里 animal 是 Cat 类型
animal.meow(); // ✅ 正确
// animal.bark(); // ❌ 错误:Property 'bark' does not exist on type 'Cat'} else {
// TypeScript 知道这里 animal 是 Dog 类型
animal.bark(); // ✅ 正确}
}
TS 类型
// Partial - 所有属性变为可选
interface Todo {
title: string;
description: string;
}
type PartialTodo = Partial<Todo>;
// Required - 所有属性变为必需
type RequiredTodo = Required<PartialTodo>;
// Readonly - 所有属性变为只读
type ReadonlyTodo = Readonly<Todo>;
// Pick - 选择部分属性
type TodoPreview = Pick<Todo, "title">;
// Omit - 排除部分属性
type TodoWithoutDescription = Omit<Todo, "description">;
// Record - 创建对象类型
type PageInfo = Record<"home" | "about" | "contact", {title: string}>;
// ReturnType - 获取函数返回类型
type T0 = ReturnType<() => string>; // string
// Parameters - 获取函数参数类型
type T1 = Parameters<(s: string) => void>; // [string]
Typescript 中 interface 和 type 的差别是什么?
| 特性 | Interface | Type |
|---|---|---|
| 语法扩展 | extends |
& (交叉类型) |
| 声明合并 | ✅ 支持 | ❌ 不支持 |
| 类实现 | ✅ 支持 | ✅ 支持 |
| 联合类型 | ❌ 不支持 | ✅ 支持 |
| 元组类型 | ❌ 不支持 | ✅ 支持 |
| 映射类型 | ❌ 不支持 | ✅ 支持 |
| 性能 | 稍好 | 稍差(可忽略) |
| 错误消息 | 更友好 | 可能显示完整定义 |
- 定义对象形状时优先使用
interface - 需要联合类型、元组或复杂类型操作时使用
type - 在库的类型定义中使用
interface以便用户扩展 - 保持一致性,在项目中统一使用约定
其他
虚拟 DOM 一定更快吗?
虚拟 DOM 并不总是更快,它的主要优势在于为开发者提供了更好的开发体验和“足够好”的性能,尤其是在复杂应用中。
虚拟 DOM 并不总是更快,甚至在简单场景下更慢。虚拟 DOM 本身也是有开销的,它需要额外的时间和内存来。例如:虚拟 DOM 需要走完“生成新树 -> Diff -> 更新”的整个流程,开销反而更大。
虚拟 DOM 应用的首次渲染需要构建虚拟 DOM 树,然后再创建真实 DOM。而直接使用字符串模板或 innerHTML 在初始渲染时可能更快。
为何渲染可视化技术的,大多数都是 canvas,而很少用 svg 的?
Canvas 基于像素的位图,即时模式,处理海量数据时图形无实体对象,性能更优,内存占用更低。
缺点:
- 需手动计算实现交互,开发复杂
- 放大易失真,需手动重绘
SVG 基于数学公式的矢量图,保留模式,每个图形都是 DOM 元素,占用较多内存。元素过多时,DOM 性能下降明显
优点:
- 每个图形自带 DOM 事件,交互实现简单
- 矢量特性,无损缩放
如果某个页面有几百个函数需要执行,可以怎么优化页面的性能?
- 任务拆分与异步化:
- 不要一次性执行完所有函数,而是每执行完一小批,就把控制权交还给浏览器,让它有机会处理用户输入和渲染,然后再继续。
- 使用
requestIdleCallback(RIC) API 专门用于执行非紧急任务,它会在浏览器 空闲时期 调用函数。非常适合那些不需要立刻完成的工作。注意:requestIdleCallback的执行时机不保证,如果页面一直繁忙,它可能迟迟得不到执行。对于有明确时间要求的任务,建议用分片策略
- 使用 Web Workers:如果这些函数是 纯计算型 的,不涉及 DOM 操作,那么 Web Workers 是最佳的解决方案。 限制:Worker 中不能操作 DOM,与主线程通信需要通过
postMessage。 - 架构与代码层面的优化
- 不要在页面初始化时就执行所有函数。使用 “按需执行” 策略,利用 Intersection Observer API 来监听元素是否进入视口。
- 函数去抖(Debounce)与节流(Throttle):如果这些函数是由高频率事件(如
scroll,resize,input)触发的,必须使用防抖或节流。 - 算法优化:是否存在重复计算?能否合并?将多个相似功能的函数合并为一个。
举例说明你对尾递归的理解,以及有哪些应用场景
在一个递归函数中,如果函数在调用自身之后,除了返回它的返回值,没有再执行任何其他操作 ,那么这个递归调用就称为尾递归。 递归调用是整个函数体中最后执行的语句,并且它的返回值直接作为当前函数的返回值。
- 阶乘函数
异步编程有哪些实现方式?
| 方式 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| 回调函数 | 最基础,函数作为参数传递 | 简单、无处不在 | 回调地狱、错误处理难 |
| Promise | ES6 标准,代表未来值 | 链式调用、错误处理更佳 | 依然比同步代码复杂 |
| Generator | 可暂停执行的函数 | 以同步方式写异步代码 | 需要执行器、语法复杂 |
| Async/Await | Promise 的语法糖 | 代码最清晰、易于调试、错误处理简单 | 必须用在 async 函数中 |
| 事件监听 | 基于事件驱动,不关心顺序 | 解耦、适合处理流和重复事件 | 容易导致“事件满天飞”、流程不直观 |
前端灰度发布
如何监控前端页面的崩溃?
window.onerror 和 try-catch:只能捕获同步和部分异步的错误,当页面进程本身因内存溢出、无限循环或复杂操作而卡死或崩溃时,这些机制就失效了。
| 方案 | 检测目标 | 可靠性 | 复杂度 | 推荐度 |
|---|---|---|---|---|
| Service Worker 心跳 | 进程卡死、无响应 | 高 | 中 | ★★★★★ |
| MutationObserver | DOM 白屏、内容异常 | 中 | 低 | ★★★☆☆ |
| ReportingObserver | 浏览器崩溃 | 很高 | 低 | ★★☆☆☆ (待普及) |
| beforeunload 对比 | 异常退出 | 低 | 低 | ★☆☆☆☆ |
建议进行组合方案,以实现更全面的覆盖,避免浏览器兼容等问题
用户访问页面自屏了,原因是啥,如何排查?
- 可能的原因
- 网络层面:资源加载失败、CDN 问题、用户网络差
- JavaScript 执行错误:语法错误、运行时错误、第三方库冲突、框架初始化失败
- 资源加载与解析:HTML 结构问题(根节点
<div id="app"></div>被意外删除或覆盖。)、CSS 加载阻塞 / 错误、主 JS 文件过大 - 环境与兼容性:浏览器兼容性
- 系统化排查方法
- 确认问题范围: 是个别用户还是所有用户?
- 本地开发环境复现与排查
- 禁用缓存: 在 Network 标签下,勾选 “Disable cache”,模拟新用户首次访问的情况,排除缓存干扰。
- 切换网络环境 / 模拟慢速网络
- 生产环境排查(用户端)
- 控制台查看
- 使用远程调试工具:如微信浏览器可以使用
vConsole,PC 端可以使用weinre或浏览器自带的远程调试功能。
- 代码层面与监控排查
- 添加错误边界(对于 React)或全局错误捕获(对于 Vue)。React: 使用
ErrorBoundary组件包裹你的根组件,在componentDidCatch方法中捕获错误并展示降级 UI,同时将错误信息上报。Vue: 使用Vue.config.errorHandler全局错误处理函数来捕获和处理错误。 - 构建分析:
webpack-bundle-analyzer等工具生成的报告,看是否有意料之外的巨大依赖包 - 接入 APM(应用性能监控):Sentry、Fundebug 等前端监控 SDK
- 添加错误边界(对于 React)或全局错误捕获(对于 Vue)。React: 使用
前端如何解决页面请求接口大规模并发问题
- 减少后端请求:例如装修页,将所有接口数据封装成一个新的接口返回 json
- 使用 HTTPS,请求头压缩
- 数据缓存:静态资源本地缓存,地区数据本地缓存
- 优化请求时序:懒加载、预加载
- 提升用户体验(降级与反馈):使用骨架屏(Skeleton Screen)、优雅降级与重试机制