Skip to main content

Account 简答类

展开/收起分类
随机HTML〡CSS
随机JS〡ES6+〡TS
随机React
随机Vue
随机小程序
随机工程化
随机浏览器丨性能
随机网络丨服务端
随机设计模式丨算法
随机全部
顺序全部
随机置顶
随机 ⭐
顺序 ⭐
5道(0)
重置
0
🌗

HTML丨CSS​

iframe 的优缺点​

iframe 用于嵌入第三方独立页,隔离很干净,但不利于 SEO,而且通信很麻烦。

对 sticky 粘性定位的理解​

sticky 在滚动未超出时表现像 relative 相对定位,在超出滚动范围后效果类似 fixed 固定定位,在父级结束滚动或设置了 overflow 时,粘性效果会失效。

fixed 什么时候不以视口为定位基准​

一旦祖先元素设置了 transform、filter 这类属性,fixed 就不再相对整个视口定位,而是哪个祖先元素设置了这些属性,就相对哪个祖先元素去定位。

怎么理解层叠上下文​

元素一旦有了层叠上下文,其层级会比普通元素高,而在层叠上下文内部,元素的层级受制于外部的层叠上下文。

⭐ 怎么理解 BFC​

BFC 是一块独立的渲染区域,其内部布局不影响外部,可以用来清除浮动和消除边距重叠。通过设置 overflow、设置浮动或设置 display 为 inline-block 和 flow-root 都可以创建 BFC。

flex:1 写法的原理​

flex 是 grow、shrink、basis 的缩写。flex: 1 等价于分别设置 1 1 和 0%,元素能伸能缩,基准尺寸为 0,按剩余空间分配。

水平垂直居中有哪些实现方式​

最常用的是 flex:父元素 display:flex,再 justify-content:center; align-items:center。也可以直接使用 Grid 网格布局来实现,直接给父元素设置 display:grid; place-items:center 即可。而传统的像绝对定位配合 margin 或 transform 去控制位置也可以。

CSS 方案怎么选型​

我现在使用 CSS Modules 或 Tailwind 这种零运行时的比较多,像 CSS Modules 就挺稳定的,它的作用域靠编译期哈希隔离;而 Tailwind 这种原子化方案不用专门起类名,可以直接复用;另外像 CSS-in-JS 这种吃运行时开销的方案,我之前也在用,他们在动态样式能力很强,但对服务端组件就不太友好。

为什么 CSS-in-JS 在 RSC 下有问题​

主要是 CSS-in-JS 依赖 Context 和 hooks,但服务端组件不支持。

主题切换(暗色模式)的最佳实践是什么​

主题切换我一般用 CSS 变量管理颜色,通过 html 的 data-theme 属性来切换,为了避免刷新时会闪一下,一般还会在 head 中用内联脚本提前读取用户主题。

最近关注哪些现代 CSS​

我知道的一些新特性有容器查询、:has()、@layer 这些,其中容器查询可以让组件根据父容器大小去适配不同的样式;而 :has() 可以根据子元素的状态反过来去控制外层样式;@layer 的话则可以显式控制优先级,避免去写 !important。

前景色自动适配的实现​

WCAG 提供了一个加权平均法的灰度转换公式,可以根据背景色的 rgb 值计算亮度,然后利用 HSL 颜色单位的亮度值,配合 CSS 动态样式给前景色设置亮色或暗色。

JS〡ES6+〡TS〡库​

JS 为什么是单线程的​

因为 JS 是为了操作 DOM 而诞生,如果是多线程的话,可能会出现两个线程同时操作一个 DOM 的情况,例如在一个线程中修改 DOM,在另一个线程中删除这个 DOM,这时浏览器就需要决定哪个线程生效,为了避免这种冲突,JS 被设计成单线程。

对 AST 抽象语法树的理解​

AST 抽象语法树是用树状的形式表现源代码的语法结构,可通过 Babel 来进行 JS 与抽象语法树的转换,添加相应的功能。

检验数据类型的方法​

typeof、instanceof、constructor、严格运算符 ===、Object.prototype.toString.call()。

instanceof 和 constructor 的区别​

instanceof 通过原型链来判断实例;而 constructor 直接指向创建该实例的构造函数。

Object.is 与 === 的区别​

Object.is 与 === 一样,都用来比较两个值是否严格相等。区别在于 Object.is 针对 +0 和 -0 会返回 false,而针对两个 NaN 会返回 true。

为什么 0.1 + 0.2 !== 0.3,怎么解决​

因为计算机用二进制存储浮点数,0.1 + 0.2 会先算二进制再转回十进制,由于存储位数有限(64 位),中间会出现舍入操作,导致计算误差。

可通过 toPrecision 控制精度,或 Number.EPSILON 误差范围判断来解决这个问题。

变量提升是什么​

JS 变量提升指的是变量和函数声明会被提升到当前作用域顶部。其中 var 和函数声明可以在声明前访问,var 读到 undefined、函数能直接调用;而 let、const、class 虽然也会提升,但会进入暂时性死区,声明前访问直接报错。

暂时性死区是什么​

暂时性死区(TDZ)指的是 let 和 const 声明之前的区域。这段时间变量其实已经提升了,只是还没初始化、处在不可访问的状态,所以声明前访问会直接报错。

作用域和作用域链是什么​

作用域指的是变量的可访问范围,分为全局作用域和局部作用域。而作用域链指的是向父级作用域逐层查找变量的链式结构。

⭐ 什么是闭包​

闭包是指内部函数引用了外部变量,只要内部函数还在被引用,这些变量就不会被回收,可以避免全局污染,但也可能造成内存泄漏。

⭐ 对内存泄漏的理解​

内存泄漏指的是程序中不再需要的内存没有及时释放,导致可用内存越来越少。例如全局变量、闭包、定时器等都可能造成内存泄露。

页面开久了内存一直涨怎么查​

可以先对比两张堆快照,看哪些对象只增不减。常见有三类:订阅、定时器和事件监听没卸掉;节点已经离开文档,却还被引用着;还有画布和历史轨迹,把缓冲越攒越大。所以离开视口就取消订阅,轨迹设个上限,缓冲尽量复用。等页面卸载时,再把定时器和连接一起清掉。

⭐ 对垃圾回收机制的理解​

V8 的垃圾回收采用分代回收,新生代用复制算法快速回收,而老生代用标记清除、标记整理和增量标记,结合并发和并行回收来提升效率。

⭐ 如何正确判断 this​

  • 普通函数独立调用时,非严格模式下 this 指向全局对象,严格模式下是 undefined;

  • 作为对象方法调用时,this 指向该对象;

  • 构造函数的 this 指向 new 出来的实例;

  • 通过 call()、apply()、bind() 绑定的函数,this 指向传入的第一个参数;

  • 而箭头函数没有自己的 this,直接继承定义时所在上下文的 this。

⭐ call()、apply()、bind() 的区别​

call()、apply()、bind() 都是用来改变函数的 this 指向,但 call 和 apply 会直接执行修改后的函数,而 bind 是返回修改后未执行的函数;另外,apply 是用数组的形式来接收参数,而 call 和 bind 是逐个传入。

⭐ 箭头函数与普通函数区别​

箭头函数书写更简洁,但不能用作构造函数,也就不能被 new 调用;而且没有 arguments 对象;另外箭头函数没有自己的 this,因此不能用 call()、apply()、bind() 等方法改变 this 指向。

new 运算符的原理​

new 的原理是创建一个新对象,绑定好原型,然后把构造函数的 this 指向这个新对象,接着执行构造函数,如果构造函数返回的是引用类型则直接作为 new 的结果,否则返回刚才创建的新对象。

Object.create() 原理​

Object.create 的原理是根据参数去创建一个新对象:第一个参数会作为新对象的隐式原型;第二个参数是属性描述符,用来给新对象定义属性,可以控制 writable、enumerable 这些特性,默认都是不可写不可枚举的。

⭐ 浅拷贝和深拷贝的区别​

浅拷贝是只复制第一层属性,可通过 Object.assign()、扩展运算符等方式实现浅拷贝;而深拷贝是递归复制每一层,与原对象互不影响。

使用 JSON.stringify 实现深拷贝有什么问题​

JSON.stringify 实现深拷贝在序列化时会产生各种失真:例如 Date 时间对象会变成字符串,正则和 Error 会变成空对象,函数会直接丢失,所以一般只拿来处理纯 JSON 结构的数据。

⭐ 什么是原型和原型链​

每个构造函数都有一个 prototype 属性,这个 prototype 属性对应的值就是原型,包含了该构造函数的属性和方法;对象读取属性时,会先在自身查找,如果找不到就沿着隐式原型逐层向上查找,形成原型链。

JS 实现继承的方式有哪些​

可通过构造函数实现继承,也可以通过原型链实现继承,还可以通过 class extends 等方式实现继承。

对函数式编程的理解​

函数式编程是一种编程范式,通过封装和组合各个函数来计算得到结果,而且过程中不会产生副作用,复用时不需要考虑它的内部实现和外部影响,提高了代码的复用性。

什么是纯函数​

纯函数指的是没有副作用的函数,相同的输入永远会返回相同的输出。

对柯里化的理解​

柯里化是一种函数式编程技术,将多参函数转为只有一个参数的函数,调用时返回一个新函数去处理剩下的参数;本质是用闭包把参数保存起来,当参数足够多时开始执行函数。可以更好的处理复杂函数,提高了代码的可读性。

function curry(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return (...args2) => curry(fn, ...args, ...args2);
}

const add = (x, y) => {
console.log(x + y)
}

const curriedAdd = curry(add)

curriedAdd(1)(2) // 3
curriedAdd(1, 2) // 3

低代码的底层原理​

低代码的原理是把页面的结构、属性和交互逻辑沉淀成一份 JSON Schema,通过可视化编辑器产出,然后运行时由渲染引擎按配置去渲染组件。

对事件委托(代理)的理解​

事件委托(事件代理)就是利用事件冒泡,给父元素添加侦听器,统一处理子元素的事件,这样就不用给每个子元素都绑定事件,可以减少内存上的消耗,而且新加的子元素也会被处理。因此,当要给一组元素添加相同事件时,就可以进行事件委托。

// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
if (e.target.nodeName.toLowerCase() === 'li') {
console.log('the content is: ', e.target.innerHTML);
}
});

target 和 currenttarget 的区别​

target 是指触发事件的元素,而 currenttarget 是指绑定事件的元素。

⭐ 对 Event Loop 事件循环的理解​

JS 的运行过程中,遇到同步任务时会直接执行,而遇到异步(宏 / 微)任务时,会先放入(宏 / 微)任务队列,等同步任务执行完再执行任务队列的异步任务,主线程不断重复这个过程,就是 Event Loop 事件循环。

⭐ 对宏任务和微任务的理解​

异步任务也包括宏任务和微任务,在 JS 的运行过程中,遇到宏任务和微任务都会先放入任务队列,等同步任务执行完,微任务会先被调入主线程中执行,然后才是宏任务。常见的宏任务包括 setTimeout、setInterval、I/O 操作等,而常见的微任务包括 Promise 的 then 回调、MutationObserver 等。Node 里的 process.nextTick 不属于微任务,它有自己独立的队列,而且优先级比微任务还高。

Node 的 Event Loop 和浏览器的区别​

Node 和浏览器的事件循环机制不一样:浏览器是按宏任务、微任务两类来分;而 Node 是按阶段跑,分 timers、pending callbacks、poll、check、close 这几个阶段,每个阶段之间会清空微任务。另外 Node 还多了个 process.nextTick,它有自己独立的队列,优先级比微任务还高。

⭐ Promise 的实现原理是什么​

Promise 的本质是个状态机,包括 pending、fulfilled 和 rejected 三种状态,状态一旦改变就不可逆。Promise 的实现基于发布订阅模式,其中 then 方法会把回调存起来,当状态变为 fulfilled 或 rejected 时依次执行这些回调;而且这些回调会被放到微任务队列中,等同步任务执行完再执行。

⭐ Promise.all() 的实现原理​

首先接收 Promise 数组,同时用一个计数器来记录完成情况,等待全部 fulfill 后按原顺序返回结果数组;如果有一个 reject,Promise.all 自身会立刻 reject,但它不会取消其他的 Promise,剩下的任务还会继续跑完,只是结果被忽略。

function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
// 存储结果的数组
const results = [];
// 已完成的 Promise 计数
let completed = 0;
// 处理空数组的情况
if (promises.length === 0) {
resolve(results);
return;
}
// 遍历每个 Promise
promises.forEach((promise, index) => {
// 将非 Promise 对象转换为 Promise
Promise.resolve(promise)
.then((value) => {
// 将结果存入对应索引位置
results[index] = value;
// 增加完成计数
completed++;
// 所有 Promise 都已完成时,返回结果数组
if (completed === promises.length) {
resolve(results);
}
})
.catch((error) => {
// 任何一个 Promise 失败,立即 reject
reject(error);
});
});
});
}
// 使用示例
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve) => setTimeout(() => resolve(2), 1000));
const promise3 = 3; // 非 Promise 对象会被自动转换为 Promise
myPromiseAll([promise1, promise2, promise3])
.then((values) => console.log(values)) // 输出: [1, 2, 3]
.catch((error) => console.error(error));

⭐ Promise.all、allSettled、race、any 有什么区别​

Promise.all 必须全部 fulfill 才成功,有一个 reject 就立刻失败;而 allSettled 永远不 reject,等所有 Promise 落定后返回每一条的状态和值;race 由第一个落定的 Promise 决定结果,不管成功还是失败;而 any 则是谁先成功用谁,全部 reject 才失败。

⭐ async/await 的底层原理​

async/await 是基于 Promise 实现的语法糖,将异步代码转为同步写法。其中,async 函数调用时会返回 Promise,遇到 await 时会挂起当前函数,等待 Promise 的结果后在微任务中继续执行。

近两年新增了哪些 JS 语法特性​

有个 structuredClone() 函数能实现深拷贝,但遇到函数、DOM 节点等无法拷贝的值会直接抛出错误;然后有一些新增的数组 API,例如 toSorted 和 toReversed 能做到排序和反转不改变原数组。

数据结构 Set 和 WeakSet 的区别​

Set 结构与数组类似,但数组内的值可以重复,而 Set 不可以;WeakSet 结构与 Set 类似,也是不重复值的集合,但 WeakSet 的成员只能是对象类型,而且这些对象是弱引用,会被自动回收。

数据结构 Map 和 WeakMap 的区别​

Map 结构与对象类似,但对象的 key 值只能是字符串,而 Map 的 key 值可以是任意数据类型;WeakMap 结构与 Map 类似,但 WeakMap 的 key 值只能是对象类型,而且这些对象是弱引用,会被自动回收。

什么是 Proxy​

Proxy 用于创建对象的代理,实现基本操作的拦截和自定义,例如 Vue3 就是用 Proxy 代理来实现响应式。

Proxy 与 Object.defineProperty 的区别​

Proxy 用于创建对象的代理,实现基本操作的拦截和自定义;而 Object.defineProperty 用于在对象上定义新属性或修改现有属性,并返回该对象。其中,defineProperty 只能劫持对象已有的属性,新增属性要再调一次,这也是 Vue2 无法跟踪响应对象新增属性的原因;而 Proxy 劫持的是整个对象,监听对象读写不需要做特殊的处理,而且还能监听到删除属性等更多操作,因此 Vue3 使用 Proxy 来重构响应式。

⭐ ES6 Module 与 CommonJS 的区别​

  • ES6 模块通过 export 导出、import 导入;而 CommonJS 模块通过 module.exports 导出,require 导入;
  • 在同步方面,ES6 导出的模块是个引用,模块内的修改会同步到外部;CommonJS 导出的模块是个快照,模块内的修改不会同步到外部;
  • 在加载时机方面,ES6 模块的导入是在编译时加载(支持 Tree-Shaking),而 CommonJS 模块的导入是在运行时才加载。

JS 如何解决模块循环引用​

把 A、B 的公共逻辑抽到 C,让他们都依赖 C;或者把 import 挪到函数中用到时再加载,避免一上来就互相依赖;

ES6 的 import 和 export 是静态还是动态的​

ES6 的 import 和 export 是静态的,在编译阶段就能确定模块的依赖关系,方便做静态分析,例如实现 Tree Shaking 消除没被用到的代码。

⭐ TS 中 type 和 interface 的区别​

interface 支持声明合并,而 type 不支持,但 type 在写联合类型、元组类型这些更为灵活;另外在继承方面,interface 使用 extends 来完成,而 type 直接使用 & 符号。

// 声明合并
interface User {
name: string;
}
interface User {
age: number;
}
// 等价于:{ name: string; age: number }
const u: User = { name: 'Tom', age: 18 };
// 联合类型
type ID = number | string;
let userId: ID = 123; // 合法,number 是 ID 的一种可能类型
let postId: ID = "abc"; // 合法,string 是 ID 的一种可能类型
// let invalidId: ID = true; // 非法,boolean 不是 ID 的可能类型
// 元组类型
type UserInfo = [string, number, boolean];
let user: UserInfo = ["Alice", 30, true]; // 合法,类型顺序和数量匹配
// let invalidUser: UserInfo = [123, "Bob", false]; // 非法,类型顺序不匹配
// let shortUser: UserInfo = ["Charlie", 25]; // 非法,元素数量不足

⭐ TS 中 any 和 unknown 的区别​

any 表示任意类型,但会跳过类型检查;unknown 也能表示任意类型,但更安全,操作前会进行类型检查,缩小类型范围。

TS 中 void 和 never 的区别​

void 表示函数没有返回值,但函数会正常结束执行;而 never 表示永远不会出现的值,即函数不会正常结束执行,一般是抛出错误或陷入死循环。

⭐ 对 TS 泛型的理解​

泛型指的是在定义时不指定具体类型,而在使用时才根据参数类型进行推断的机制。TS 中可以用尖括号的形式来使用泛型。

TS 中的类型断言和类型守卫是什么​

类型断言是直接将类型告诉编译器,而类型守卫是一种运行检查机制,通过 typeof 等方法检查判断,缩小类型范围。

TS 中 Pick 的作用​

Pick 用于从对象类型中选出一部分属性,生成新类型。

type User = {
name: string;
age: number;
email: string;
};
// 从 User 类型中只选择 'name' 和 'age' 属性
type UserBasicInfo = Pick<User, 'name' | 'age'>;
// 相当于:
type UserBasicInfo = {
name: string;
age: number;
};

TS 中 Partial 的作用​

Partial 用于把对象类型的所有属性变为可选;

type User = {
name: string;
age: number;
email: string;
};
// 将 User 的所有属性变为可选
type PartialUser = Partial<User>;
// 相当于:
type PartialUser = {
name?: string;
age?: number;
email?: string;
};

TS 中 infer 的作用​

infer 用于在条件类型中推断类型。

// 定义一个条件类型,用于提取函数的返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 使用 ReturnType 提取函数的返回值类型
type Num = ReturnType<() => number>; // 结果:number
type Str = ReturnType<(x: string) => string>; // 结果:string

TS 的 strict 模式你们开了吗​

新项目的话我会直接开 strict 模式,在编译期强制处理 undefined、any 等问题;如果是老项目的话,直接开启会有很多报错,因此一般只有新代码去走 strict,老代码会分批兼容治理。

大仓库全量 tsc 太慢怎么办​

先开 incremental 做增量检查,只检查改过的文件;如果是大 monorepo,则用 project references 拆包,这样改一个包就不用整个仓库重编了。

React​

React 18 有哪些新特性​

React 18 引入新的 Render API,支持并发模式的渲染,可以用 Transition API 进行并发控制;而且 React 18 支持自动批处理,在 setTimeout、setInterval、addEventListener 等原生事件中也会进行合并批量更新,以提升性能;另外,React 18 还用 Suspense 组件为服务端渲染提供更快的页面加载。

useTransition 和 useDeferredValue 的区别​

useTransition 是告诉 React 这次更新不紧急,可以拿到 isPending 来显示加载中状态;而 useDeferredValue 是暂时用旧的值去渲染,等新的值拿到再更新上去。两者都常用于搜索框,输入都是实时的,但结果慢半拍。

React 19 有什么升级​

React 19 对 form 标签进行了加强,允许传递 action 来处理表单逻辑,而且新增了 useActionState 和 useFormStatus 等钩子,可以更灵活的处理表单的加载状态,还有就是内置乐观更新的支持,新增 useOptimistic 钩子显示乐观状态;另外,React 19 对 Context API 简化了 Provider 的写法;对 ref 回调新增了清理功能,允许在组件卸载时自动执行清理逻辑。

React 19 的 use() 是干什么的​

use() 是 React 19 中用来读取 Promise 或 Context 的 API,它不算 Hook,所以可以在条件判断和循环中调用。当 use 读取 Promise 时会暂停组件的渲染,配合 Suspense 去展示加载中状态。

什么是乐观更新​

乐观更新是假设异步操作会成功,并立即将 UI 更新到操作成功后的状态,而不是等到操作完成。以 useOptimistic 钩子为例,如果操作失败,useOptimistic 会自动将界面恢复到原来的样子,就像什么都没发生过一样,可以让用户感觉应用反应很快,体验更流畅。

⭐ React 的渲染机制​

React 的渲染机制是当 state 或 props 发生变化时,React 不会重新渲染整个页面,而是在渲染阶段跑 diff 算法找到需要更新的元素,然后在提交阶段一次性更新到页面中。

⭐ 对虚拟 DOM 和 diff 算法的理解​

虚拟 DOM 是用 JS 模拟 DOM 结构,避免 DOM 树的频繁更新,从而提升页面渲染性能。而 diff 算法就是用来高效地对比新旧虚拟 DOM,找出真实 DOM 的变化之处。

⭐ 对 React 中 diff 算法的理解​

React 的 diff 算法使用三大策略来降低 diff 算法的复杂度,首先只对同一层级的节点进行比较;其次,如果比较的组件是同一类型,则继续往下进行 diff 运算,如果不是同一类型,则直接拆掉子树重建;最后,同层级的一组子节点,还会通过 key 值区分复用。除了三大策略优化 diff 算法外,React 还引入了 Fiber 架构,比 Vue 的 diff 算法多了时间切片的能力。

⭐ React key 值的作用​

React 遍历节点时需要给每个节点设置一个 key 值作为唯一标识,使 diff 算法可以正确识别此节点,找到正确的位置插入新节点,从而高效的更新虚拟 DOM。

⭐ React 中 key 值需要注意的地方​

key 值不能重复,而且不能使用对象或数组等非基本类型作为 key 的值;另外,不建议使用索引作为 key 值,因为列表节点的顺序可能发生变化,直接使用索引作为 key 值会影响性能或出现 bug。

⭐ 对 React Fiber 的理解​

Fiber 是 React 16 开始使用的协调引擎,把原来一次递归到底的更新,拆分成任务切片来做,中间可以暂停,让出主线程去处理高优任务。它内部维护 current 和 workInProgress 两棵 Fiber 树做双缓冲,先在内存里把新树构建好,再在 commit 阶段一次性更新到真实 DOM,解决调度卡顿的问题。

为什么 React 需要 Fiber,而 Vue 不需要​

因为 React 默认会递归渲染整棵子树,容易堵住主线程,所以需要 Fiber 做切片和优先级调度;而 Vue 靠 Proxy 做依赖收集,数据变了能精确知道是哪个组件用到了它,更新是组件级同步进行的、通常量不大,所以不需要切片。

⭐ setState 的调用原理 / 调用 setState 发生了什么​

调用 setState 时,React 会创建更新对象,推到 Fiber 的更新队列,同一批次的多次更新会合并。接着走协调流程:在渲染阶段跑 diff 算法找到需要更新的元素,然后在提交阶段一次性更新到页面中。

⭐ setState 批量更新策略是怎样的 / 是同步还是异步​

setState 表现像异步,但它只是批量延后,本身是同步执行的,等当前事件回调结束后会进行合并批量更新,可以通过 flushSync 强制同步刷新。

React 的事件机制是什么样的​

React 的事件不是绑定到真实 DOM 上的,而是通过事件代理的方式,将合成事件统一绑定到 Root 元素上。其中,合成事件是 React 模拟原生事件的一个事件对象,可以兼容不同的浏览器,而且方便统一分发。但合成事件无法通过 return false 阻止默认行为,需要使用 preventDefault() 来实现。

⭐ React Hooks 解决了什么问题​

原有的函数组件也被称为无状态组件,只负责渲染工作,而引入 React Hooks 后,函数组件也可以是有状态的组件,可以维护自身的状态、做一些逻辑处理。

⭐ Hooks 的实现原理​

Hooks 的状态挂在 Fiber 的 hook 链表上,按调用顺序排列,所以 Hook 必须写在顶层,如果放进条件判断或循环中顺序会错位,导致状态对不上。

⭐ useEffect 的返回值有什么用​

useEffect 可以返回一个函数,在组件卸载时执行,用来清理副作用。

⭐ useEffect 与 useLayoutEffect 的区别​

useEffect 在浏览器 DOM 绘制完成之后执行;而 useLayoutEffect 在浏览器 DOM 绘制之前执行,会阻塞浏览器的绘制,一般在需要测量或修改 DOM 布局时使用。

useEffect 的回调为什么不能是 async 函数​

useEffect 的返回值必须是清理函数或 undefined,而 async 会返回一个 Promise,无法当作清理函数使用。

⭐ useCallback 与 useMemo 的区别​

useCallback 与 useMemo 类似,都是通过缓存来提升性能,区别在于 useCallback 缓存的是函数的引用,返回一个未执行的函数;而 useMemo 缓存的是函数的返回值,返回函数执行的结果。

⭐ 什么时候不该用 useMemo / useCallback​

我现在比较少专门去用,尤其是一些简单计算,用了反而导致每次都要检查依赖是否有变化,导致更慢,而且我们项目接了 React Compiler,这些编译器会自动去优化。

⭐ 定时器里读 state 永远是旧值,怎么解​

定时器只认创建时那次渲染的 state,所以一直是旧的,可以用 useRef 存最新值,在定时器中去读 ref,以解决这个问题。

⭐ React 竞态请求怎么处理​

可以先创建 AbortController 对象,然后把 signal 取消信号传给 fetch 或 Axios,在清理函数中调用 AbortController 的 abort 方法取消旧请求,确保只有最后一次请求能落地。

React 组件之间有哪些通信方式​

父子组件可以通过 props 配合回调完成通信;兄弟组件可以将状态提到共同父组件去完成通信;而跨好几层、又不常变化的,可以用 Context 进行通信。如果是全局状态,可以用 Zustand 或 Redux 去通信。

React Context 的原理​

Context 的原理是在 Provider 渲染时,把新的 value 写进 Context,然后子组件 useContext 底层走 readContext 去取值,并在 fiber 上记下依赖。当 value 的值变化时,消费它的组件会跟着更新。

父组件怎么调用子组件的方法​

子组件可以通过 useImperativeHandle 暴露方法,然后父组件通过 ref 去调用。React 19 之前子组件还要额外用 forwardRef 去包一层才能接到 ref。

React 如何将节点挂载到任意位置​

通过 ReactDOM.createPortal(child, container) 方法实现。

⭐ 如何理解受控和非受控组件​

受控组件的值由 React 进行管理,通过 value 传值配合 onChange 回写;而非受控组件的值由 DOM 自己管理,需要时再通过 ref 去读取。

类组件与函数组件的区别​

类组件基于面向对象编程,需要继承 React.Component,有 this 和生命周期;而函数组件是纯函数,接收 props 并返回 JSX,配合 Hook 去管理状态和副作用。

React 类组件有什么是函数组件替代不了的​

错误边界目前还得靠类组件,通过 getDerivedStateFromError 决定出错后展示什么,通过 componentDidCatch 打日志,这些函数组件都没有对应 Hook;另外 getSnapshotBeforeUpdate 这类生命周期可以在 DOM 更新前抓快照,Hook 也替代不了。

React 组件的生命周期​

  • 挂载阶段为 constructor()、render() 和 componentDidMount()

  • 更新阶段为 render() 和 componentDidUpdate()

  • 卸载阶段为 componentWillUnmount()

⭐ React 什么时候会重新渲染组件​

当 props、state、Context 或 store 发生变化、以及父组件渲染时都会导致组件重新渲染。

⭐ React 性能优化 / 如何避免不必要的渲染​

首先通过 React DevTools 的 Profiler 看谁在频繁渲染,然后如果是类组件,可以通过 shouldComponentUpdate 方法或继承 PureComponent 的方式来优化;如果是函数组件,则可以通过 React.memo、useCallback 或 useMemo 来进行优化。

React 中如何实现懒加载​

对于组件的懒加载,React 跟 Vue 的 defineAsyncComponent 一样,提供了 React.lazy 用于动态导入组件,再配合 Suspense 展示加载组件,可以实现懒加载;而对于路由的懒加载,react-router 也是可以通过 React.lazy 配合 Suspense 来实现懒加载。

React 中 Suspense 组件的作用​

Suspense 组件用于处理异步加载的场景,通过 fallback 属性指定加载中的占位内容,加载完成后会渲染 Suspense 包裹的内容。

React 的错误边界是什么​

错误边界捕获子组件树渲染期间的错误,通过 getDerivedStateFromError 决定出错后展示什么,通过 componentDidCatch 打日志。

说说 React Compiler​

React Compiler 是官方提供的编译器,它会在编译期分析组件数据流,自动插入等价的 React.memo、useMemo 和 useCallback 去完成优化。

⭐ 对服务端渲染的理解​

SSR 服务端渲染指的是在服务端完成 HTML 的渲染,再返回给浏览器解析;可以提高首屏加载速度,而且有利于 SEO;但会增加服务器的负担。

⭐ 什么是水合,水合不一致有哪些常见原因​

水合是服务端把 HTML 发过来之后,客户端再把事件和状态绑定到 DOM 上,让页面能正常交互的过程;水合不一致指的是服务端和客户端首次渲染对不上,例如时间、随机数等数据不统一都会导致水合不一致;可以通过先挂载后渲染来解决。

SSG、SSR、ISR、CSR 的区别​

SSG 静态站点生成是在构建阶段提前生成 HTML,适用于博客这类数据几乎不变,但对 SEO 有要求的页面;SSR 服务端渲染是每次请求都在服务端生成 HTML 再返回,适用于商品详情这类每次请求都要求最新,且对 SEO 有要求的页面;ISR 是静态页定时在后台更新,适用于文章列表这类数据量大,且偶尔需要更新的页面;CSR 客户端渲染是靠浏览器跑 JS 进行渲染,适用于管理后台这类对 SEO 没要求的页面。

说说 RSC(React Server Components)​

RSC 是组件在服务端跑,浏览器只接收结果,所以前端包体积更小。

说说 Server Actions​

Server Actions 就是在服务端写异步函数,用 use server 标记,挂到表单 action 或按钮事件中,自动发到服务端执行。

Next.js 的缓存机制是怎样的,revalidatePath 和 revalidateTag 有什么区别​

Next.js 的缓存分几层:同一个请求里相同的 fetch 会去重;Data Cache 是跨请求的数据缓存,按 URL 和参数做键;Full Route Cache 是整个路由渲染出来的 HTML 和 RSC 数据;客户端还有一层 Router Cache 管导航。Next.js 15 之后 fetch 默认不缓存了,要缓存得显式声明。刷新的话,revalidatePath 是按路径失效,revalidateTag 是按标签失效——标签更精准,可以一次让多个路由一起失效。

App Router 和 Pages Router 怎么选​

新项目可以直接用 App Router,页面默认在服务端生成,数据写在组件中,而且布局按路由嵌套;老项目的话页面还是走客户端,继续用 Pages Router。

⭐ React 状态管理怎么选型​

服务端接口数据可以用 TanStack Query 管理,前端全局状态如果是大项目可以用 Redux Toolkit 管理,中小项目可以用 Zustand 管理,剩下表单和组件内局部状态就直接用 useState。

Redux 和 Zustand 区别​

Redux 强制走 dispatch action 到 reducer 的数据流,通过中间件处理异步,适合大团队统一规范;而 Zustand 更轻量,没有 Provider,创建一个 store 就能直接用,写法更简单。

MobX 和 Redux 怎么选,MobX 的响应式是怎么实现的​

MobX 取值时会自动记下依赖关系,更新的时候只通知用到的地方,像影石购买页中各种字段堆在一起,改一个字段好几处都要跟着变,就适合用 MobX;而如果要一步步查出是谁改的,则用 Redux 更合适。

什么情况下会用到 Context​

Context 适用于要跨好几层、又不常变化的,像主题、语言这些。

Redux 的工作流程​

Redux 将公共数据存放在 Store 中,组件不能直接改 state,只能 dispatch 一个 Action,然后 Reducer 根据 Action 算出新的 state,Store 更新后通知订阅的组件重新渲染,从而实现跨组件数据共享。

对 Redux 中间件的理解​

Redux 中间件用于拦截 dispatch Action,添加其他功能,例如处理异步请求和日志监控等等。中间件的本质是函数柯里化,Redux 通过 applyMiddleware 将各个中间件串起来,最后落到 store 的 dispatch 上。

React-Router 的实现原理​

React Router 的原理是通过 JS 监听 URL 的变化,在不刷新页面的情况下去更新内容,v6 之后默认用 history 模式。其中:hash 模式通过 hashchange 事件监听 url 的 hash 值来实现路由跳转;而 history 模式则利用 pushState 和 replaceState 来修改浏览器的历史记录栈,并通过 popState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以 history 模式还需要服务端配置,否则刷新会 404。

hash 模式与 history 模式的区别和原理​

hash 模式会在 url 中夹带上 #,而且兼容性比 history 模式更好;另外,hash 模式通过 hashchange 事件监听 url 的 hash 值来实现路由跳转;而 history 模式则利用 pushState 和 replaceState 来修改浏览器的历史记录栈,并通过 popState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,所以 history 模式还需要服务端配置,否则刷新会 404。

React 开发遇到过哪些坑​

感觉 useEffect 的坑不少,比如 useEffect 一边改状态,一边又把会跟着变化的值写进依赖数组中,自己触发自己,就会导致死循环,解决办法是把依赖数组中不该依赖的值拿掉;另外 useEffect 中如果加了定时器或事件监听,但组件卸载时没及时清理,会导致页面越用越卡,解决办法是在 return 的清理函数中去清除对应的定时器或监听。

Vue​

Vue 的生命周期​

Vue 生命周期指的是 Vue 实例从创建到销毁的过程,包括:

  • beforeCreate、created;

  • beforeMount、mounted;

  • beforeUpdate、updated;

  • beforeUnmount、unmounted。

Vue 中 created 和 mounted 的区别​

created 在创建实例后调用,此时 DOM 还未生成,而 mounted 在 DOM 完成挂载后调用,可以进行 DOM 操作。

Vue 和 React 的区别​

他们响应式不同,Vue3 用 Proxy 自动收集依赖,数据变了通知对应组件更新,而 React 靠 setState 触发渲染,子组件会跟着一起渲染;另外他们写法也不同,Vue 用模板语法和 v-model 语法糖,而 React 用 JSX 语法,强调单向数据流;

Vue 模板渲染 / 挂载原理​

Vue 的模板会先被编译,解析成 AST 抽象语法树,再生成 render 函数,挂载时会执行这个 render 函数得到虚拟 DOM,渲染器再把它挂到真实 DOM 上,之后数据变了会重新 render,走 patch 做 diff 更新。

⭐ Vue 如何是实现响应式的​

Vue2 采用数据劫持结合发布订阅模式的方法,通过 Object.defineProperty() 来劫持各个属性的 getter 和 setter,在数据变化时发布消息给订阅者,触发相应的监听回调,但无法跟踪响应对象新增属性的操作。而 Vue3 采用 Proxy 代理来实现响应式,data 中的数据不再进行响应式追踪,而是转为 Proxy 代理进行追踪更新。

为什么 Vue3 要用 Proxy 重构响应式​

Proxy 用于创建一个对象的代理,实现基本操作的拦截和自定义;而 Object.defineProperty 用于在对象上定义新属性或修改现有属性,并返回该对象。其中,defineProperty 只能劫持对象已有的属性,新增属性要再调一次,这也是 Vue2 无法跟踪响应对象新增属性的原因;而 Proxy 劫持的是整个对象,读写可以统一拦截、无需特殊处理,而且还能监听到删除属性等更多操作,覆盖的能力更全,因此 Vue3 使用 Proxy 来重构响应式。

Vue3 有哪些更新​

Vue3 使用 Proxy 来重构响应式,解决了 defineProperty 无法跟踪响应对象新增属性的问题;而且新增了 Composition API,可以更灵活的组织逻辑;另外,Vue3 还在 Diff 算法、静态提升和事件监听缓存等方面进行了优化,大幅提升了性能。

Vue3 在性能方面进行了什么优化​

Vue 3 对 diff 算法进行了优化,新增了静态标记(PatchFlag),只对比带有标记的节点,减少比较次数以提升性能;Vue 3 还对不参与更新的元素做静态提升,只创建一次,在渲染时直接复用,避免节点的重复创建;另外,Vue3 还做了事件监听缓存优化,将事件缓存起来复用,以提升性能。

对 Vue diff 算法的理解​

Vue2 通过双端对比,将新旧节点从头尾两端交叉比较,命中就移动节点;而 Vue3 也是先从头尾向中间比对,能对上的先跳过,中间乱序那段则用最长递增子序列算法,减少 DOM 的移动次数。

v-show 和 v-if 的区别​

v-show 是通过切换 display 属性来控制显示隐藏,而 v-if 是控制元素是否渲染在页面中,有更高的切换开销,因此 v-show 更适合频繁切换显示的场景。

v-model 双向绑定的原理​

v-model 用于实现表单元素的双向绑定,本质是个语法糖,在父组件中传递 modelValue 给子组件,在子组件中通过 update:modelValue 事件来更新数据。

reactive 的原理​

reactive 的原理是通过 createReactiveObject 方法校验目标对象类型,然后根据类型进行依赖的收集和触发,最后返回生成的 Proxy 响应式对象。

ref 的原理​

ref 的原理是通过类的 get 和 set 进行依赖的收集和触发,完成响应式追踪,最后返回 ref 响应式对象。

reactive 和 ref 的区别​

reactive 和 ref 都用来实现响应式数据。但 reactive 接收的是引用类型,而 ref 既可以接收基本类型、也可以接收引用类型。在使用方面,reactive 对象可以直接使用,而 ref 对象在操作时需要使用 .value 属性。而在内部实现方面,reactive 通过 createReactiveObject 方法校验目标对象类型,然后根据类型进行依赖的收集和触发,最后返回生成的 Proxy 响应式对象;而 ref 通过类的 get 和 set 进行依赖的收集和触发,完成响应式追踪,最后返回 ref 响应式对象。

computed 和 watch 的区别​

computed 根据依赖进行计算并返回结果;而 watch 用于监听指定数据源,在数据源变化时执行回调函数。

computed 的原理​

computed 根据依赖进行计算并返回结果,本质是个惰性求值的观察者,只在 getter 取值操作时收集依赖,当依赖变化时通过 dirty 变量进行标记,判断是否需要重新计算,最后返回一个 ref 实例。

watch 的原理​

watch 用于监听指定数据源,在数据源变化时执行回调函数;原理是根据数据源类型构造 getter 函数,再创建 ReactiveEffect 收集依赖,依赖更新时走调度器触发更新任务,重新跑 getter 拿新值,和旧值比对后有变化再执行传入的回调函数。

watch 和 watchEffect 的区别​

watch 用于监听指定数据源,在数据源变化时执行回调函数;而 watchEffect 会立即执行回调函数,同时自动收集依赖的数据源,当数据源变化时再次执行回调函数,而且 watchEffect 的返回值还可以用来停止监听。另外 watch 可以访问数据源变化前的值,而 watchEffect 不可以。

Vue 父子组件通信的方式有哪些​

父组件可以通过 props 或 provide 配合 inject 的方式给子组件传值;而子组件可以通过 emit 事件或作用域插槽的方式给父组件传值。

Vue provide inject 的原理​

provide inject 的原理是利用原型链来实现跨级传参,当 inject 获取 provides 对象时,会沿着原型链逐层向上查找。

什么是作用域插槽​

作用域插槽指的是子组件给插槽内容传递数据,在父组件中可以通过 v-slot 指令拿到子组件插槽传过来的数据。

<!-- 子组件 -->
<slot txt="123" :count="111" />
<!-- 父组件 -->
<Comp v-slot="slotProps">
<button>{{ slotProps.txt }}</button>
<button>{{ slotProps.count }}</button>
</Comp>

对 Vue nextTick 的理解​

Vue 在响应式数据改变时,会开启一个调度队列缓冲所有数据变更,等当前同步逻辑跑完再统一更新视图;而 nextTick 就是把回调放到这次更新之后执行,底层用的是 Promise 微任务,所以能在数据改变后拿到更新后的 DOM。

对 KeepAlive 的理解​

KeepAlive 用于在动态切换组件时缓存实例,避免状态丢失。被 KeepAlive 缓存的组件移除时,生命周期不会触发 unmounted,而是触发 deactivated,从缓存里恢复时会触发 activated 而不是再次 mounted——注意首次挂载也会走一次 activated。

Vue 中如何实现懒加载​

对于组件的懒加载,Vue 跟 React.lazy 一样,提供了 defineAsyncComponent 用于动态导入组件,再配合 Suspense 展示加载组件,可以实现懒加载;而对于路由的懒加载,vue-router 中可通过箭头函数配合 import() 语法动态导入来实现懒加载。

Vue 中如何处理错误边界​

全局的错误可以通过 app.config.errorHandler 来统一处理,组件局部的错误可以通过 onErrorCaptured 来进行捕获。

Vuex 与 Pinia 的区别​

Pinia 舍弃了 Mutation,直接用 Action 来操作 State,而且不需要通过 dispatch 调用,语法更加简洁;另外 Pinia 支持多个 store,而 Vuex 得靠 module 来进行模块拆分;还有就是 Pinia 对 TS 更友好。

Vue-router (路由) 的原理​

Vue Router 的原理是通过 JS 监听 URL 的变化,在不刷新页面的情况下去更新内容,官方推荐 history 模式、hash 模式主要做兼容。其中:hash 模式通过 hashchange 事件监听 url 的 hash 值来实现路由跳转;而 history 模式则利用 pushState 和 replaceState 来修改浏览器的历史记录栈,并通过 popState 事件监听历史记录的变化来实现路由跳转,由于是单页面应用,history 模式还需要服务端配置,否则刷新会 404。

Vue Router 路由守卫是什么​

vue-router 路由守卫提供了一些钩子函数,用于在路由跳转的各个阶段执行指定操作。

移动端〡小程序​

⭐ 移动端适配方案​

设计稿一般是固定宽度的,但我们还原时需要去适配不同设备,我一般会先设好 viewport,然后开发时就正常写 px,再通过 PostCSS 在编译期把 px 转成 vw,这样页面就可以跟着屏幕等比缩放了。

移动端 1 像素问题的原因及解决方案​

当设备 DPR 大于 1 时,1 个逻辑像素会对应多个物理像素,造成视觉上的粗线,可以用伪元素去画线,配合缩放实现一个物理像素的边框。

JSBridge 的通信原理是什么​

JSBridge 就是 H5 和原生之间的一条通信通道。H5 调原生:Android 用 addJavascriptInterface 注入对象,JS 直接调上面的方法,同步的;iOS 的 WKWebView 往 messageHandlers 发 postMessage,异步、拿不到返回值,要靠 callbackId 加回调池收结果。原生调 H5,用 evaluateJavaScript 回传。想要同步拿原生结果,一般走 prompt 拦截——prompt 是阻塞式的,原生能在回调里直接把结果返回给 JS。

小程序和 H5 / 原生 App 有什么区别​

小程序运行在微信环境中,有自己的生态,能用支付、扫码、分享等微信能力。跟 H5 比,小程序不需要操心浏览器的兼容性;而跟原生 App 比,小程序开发成本低,但系统权限受限。

小程序的原理​

小程序是双线程架构,逻辑层跑 JS 代码,视图层用 WebView 进行渲染,二者通过微信客户端中转通信。

setData 为什么慢,怎么优化​

setData 慢是因为小程序逻辑层和渲染层分开,数据要先打包再通过微信中转,数据越大调用越慢。可以通过合并多次更新,控制调用频率等方式优化。

小程序如何实现组件间的通信​

父组件通过 properties 向子组件传递数据,子组件通过 triggerEvent 触发事件向父组件传值,从而实现父子组件通信;也可以通过 app.js 的 globalData 来获取和设置全局数据,实现全局数据共享;还可以创建一个事件总线对象,在不同组件之间触发和监听事件,实现数据传递。

小程序的分包加载​

小程序主包不能超过 2M,单个分包也不能超过 2M,所有分包加起来不能超过 20M;可以按业务拆成子包,启动只加载主包,进到对应页面再下载子包,还能配合预下载让体验更顺,以解决主包体积的限制问题。

小程序首屏加载怎么优化​

小程序首屏慢可能是包太大、渲染慢或数据慢导致的。如果是包太大可以把主包压小;如果是渲染慢可以上骨架屏;如果是数据慢可以提前拉接口数据,先用缓存渲染再更新。

小程序登录流程​

小程序登录流程是前端调用 wx.login 拿到临时凭证 code 发给后端,后端找微信去换 openid,再签发 token 给前端存起来,之后每次请求会带上。

Uni-app 多平台运行的原理​

Uni-app 编译器把 Vue 代码分别编译成小程序、H5 和 App 各端代码,再用一套 API 多端适配抹平各端差异,实现多平台运行。

Uni-app 的条件编译是什么​

Uni-app 的条件编译可以通过特殊注释标记,将指定的代码编译到对应平台。

同一套界面要跑浏览器和小程序,怎么拆​

计算、校验和接口数据尽量共用。页面结构、路由、登录和设备能力则按端分开,因为小程序的逻辑层和视图层是分开的,没有完整的浏览器接口。这些差异收进条件编译或一层适配里,这样业务代码就不用到处判断平台。浏览器里的 DOM,到了小程序也要换成它自己的接口。

React Native 和 Flutter 有什么区别,怎么选​

两个的底层思路不一样:RN 是用 JS 写逻辑,渲染的是原生组件,Web 前端的心智基本能复用;Flutter 是用 Dart 走自绘渲染,UI 一致性强,但要重新学一套语言和框架。选型主要看几件事:一是团队背景,前端团队上手 RN 最快;二是要不要热更新,RN 有 EAS Update 这类方案,Flutter 官方不支持代码热更新;三是看性能和原生质感的诉求。另外 RN 新架构那套现在已经在高版本里全量落地了,老架构的通信瓶颈基本不存在。

前端工程化​

说说 git rebase​

git rebase 变基操作是将提交到某一分支上的修改应用到另一分支上,可以让提交历史更干净但会重写提交记录,一般只在个人分支上使用,公共分支还是老实用 merge,不然可能导致其他人的本地历史和远程对不上。

git revert 和 git reset 的区别​

git revert 是用新的提交抵消之前的修改,而 git reset 是直接进行版本回退。

⭐ 对前端工程化的理解​

前端工程化可以理解为模块化拆代码,组件化拆页面,规范化管住质量与协作,自动化把构建、测试、发布交给工具和流水线。主要就是将容易出错的环节交给工具和规范,让整体流程更可控。

对 CI/CD 的理解​

CI 是代码合入后自动检查和测试,尽早发现问题;而 CD 包括持续交付和持续部署,其中持续交付上线前要人工确认,而持续部署测试通过自动上线。

你的组件单测/测试用例是怎么写的​

我一般按用户具体操作来测,例如通过 React Testing Library 配合 Jest 匹配对应元素,断言渲染和交互结果,而不看内部实现。

如何解决多错误场景下的 Toast 合并显示问题​

可以在全局维护一个队列,记录当前有没有提示;对同一类型的提示进行合并,避免重复弹出;如果有多种类型的提示则展示优先级最高的,等它关闭后再弹出剩下的提示;

一个成熟的前端发布流程是什么样的​

首先在合入前过自动化检查,测试和构建都过了才放行,然后产物带上版本号,上线前先灰度,盯错误和性能,没问题再逐步放量,同时留功能开关和一键回滚,确保出问题能快速止损。

⭐ 如何捕获项目中的错误​

可通过 try catch 或 window.onerror 捕获同步错误;未处理的 Promise 拒绝要用 unhandledrejection 事件来捕获,资源加载失败则要在捕获阶段监听 error。另外 React 可以通过错误边界组件捕获渲染错误,Vue 可以通过 app.config.errorHandler 来捕获。

⭐ pnpm 有什么优势​

pnpm 用链接复用依赖,同一个包整个电脑只存一份,安装快而且省磁盘。另外相比较 npm,pnpm 没声明的包引不到,能避免幻影依赖的问题。

⭐ 说说 Monorepo 的收益、代价和工具选择​

Monorepo 就是把多个包放在同个仓库中管理,可以统一规范和复用代码,但 CI 缓存、版本管理和发布会变复杂。工具上可以用 pnpm Workspace 管理依赖,用 Turborepo 或 Nx 做构建编排和缓存。

⭐ 什么是幻影依赖,为什么严重​

幻影依赖指的是在代码中用到了没在 package.json 中声明的包,本地能跑起来,是因为别的包依赖它,安装时顺便带进来了,很容易因为别的包改变而挂掉。而 pnpm 会从机制上拦截这种引用,在开发阶段直接报错。

peerDependencies 是干嘛的​

peerDependencies 用来声明当前包正常运行时所依赖的包版本,主要用于避免重复安装和版本冲突。

Prettier 和 ESLint 有什么区别​

Prettier 专注于代码格式化,保证代码风格统一;而 ESLint 用于检查代码质量,保证语法准确,两者可结合使用。

对 babel 的理解​

babel 是一个 JS 编译器,用来转换和兼容语法。原理是读取 JS 文件中的字符流,然后通过词法解析生成 Token,再通过语法解析将 Token 转为 AST 抽象语法树,接着遍历抽象语法树的节点,进行分析和转换,得到新的抽象语法树,最后编译成向后兼容的代码。

浏览器兼容性怎么保证​

可以通过构建工具来兜底,例如用 Babel 来转换和兼容 JS 语法,用 core-js 来补新的 API,用 PostCSS 的 Autoprefixer 自动加前缀来兼容 CSS,以解决浏览器的兼容问题。

Webpack 的打包 / 构建原理​

webpack 是一个 JS 打包工具,原理是从入口出发,顺着 import 递归解析依赖,构建依赖关系图,然后模块经 loader 转译后组装成一个个 chunk,最后输出为 bundle 文件,配合代码分割实现按需加载。

loader、plugin、bundle、chunk、module 分别是什么​

  • loader 是转换器,用于转译模块内容;

  • plugin 是插件,用于在构建流程中扩展能力;

  • bundle 是 webpack 打包出来的文件;

  • chunk 是 webpack 进行模块依赖分析分割出来的代码块;

  • module 是模块,一个模块对应着一个文件。

对 HMR 热更新的理解​

HMR 热更新指的是在不刷新页面的情况下去更新修改后的模块。原理是通过 HMR Server 监听模块的变化,生成记录更新的 manifest 和更新包,然后通过 WebSocket 通知浏览器,浏览器内置的 HMR Runtime 会拉取这两个文件完成模块更新。

如何提⾼ Webpack 的打包 / 构建速度​

首先可以打开 Webpack5 的持久化缓存,提升二次构建速度;其次可以通过 include 和 exclude 来收窄 loader 的处理范围,提高匹配速度;另外,Webpack 打包是单线程的,可以通过 thread-loader 开启多线程打包,让 Webpack 同时处理多个打包任务,充分利用多核 CPU 的优势,提升构建速度。

如何⽤ Webpack 来优化前端性能​

可以通过 Terser 压缩产物,也可以通过 Tree Shaking 删除未被引用的代码,再通过动态 import 和 splitChunks 做代码分割、按需加载,从而提升性能。

代码分割和包体积你怎么治理​

代码分割我一般通过动态 import 和 splitChunks 来做,只加载当前页的代码;而包体积治理我会先看分析器中最大的一块来自哪里,再决定是要拆页面还是拆第三方库。

i18n 语言包怎么按需加载和切换​

默认语言放进首包,其他语言按需拉取,如果拉取失败则回退到默认语言的文案。

Webpack5 有哪些升级​

webpack5 加入持久化缓存来提高构建性能;而且对 tree shaking 进行了优化,减小了打包体积;另外还引入了模块联邦功能,允许在多个应用之间共享代码,实现微前端等场景的依赖复用。

Webpack 的持久化缓存是什么​

Webpack 通过配置 cache 的 type 为 filesystem 来开启持久化缓存,开启后会缓存编译结果到磁盘或内存中,以减少重复编译,提升构建速度。

⭐ 对 Tree Shaking 的理解​

Tree shaking 用来删除未被引用的代码,减小打包体积以提升性能,它依赖 ES Module 的静态结构,在编译时检查代码的导入导出,在构建时标记哪些导出没被使用,最后在压缩阶段进行删除。

如何实现 Tree Shaking​

可以在 package.json 中配置 sideEffects,标记文件是否有副作用;然后在打包工具中按生产模式去构建,以启用 Tree Shaking。

Tree Shaking 为什么会失效​

Tree Shaking 失效,可能是因为代码不是标准 ES Module,比如使用了 CommonJS,导致打包器无法静态分析;也有可能是模块存在副作用,或 package.json 的 sideEffects 标记不准确,导致打包器不敢删除未使用的代码。

对 Webpack 模块联邦的理解​

Webpack 的模块联邦允许在多个应用之间共享代码,实现微前端等场景的依赖复用。

⭐ Webpack 与 Vite 的区别​

Webpack 开发时也要先打包,热更新要走构建链路,项目越大越慢;而 Vite 开发时不打包,走原生 ESM 按需转译并精准热更新,所以更快。

现在的构建工具格局是什么样的​

现在的构建工具底层基本都换成 Rust 了,性能提升很大。如果是新项目可以用 Vite 8,开发和生产都是 Rolldown;如果是老 Webpack 项目可以用 Rspack,它兼容 Webpack 的 API,迁移成本低;如果是 Next 全家桶则直接用内置的 Turbopack。

⭐ Vite 的实现原理​

Vite 现在用 Rust 编写的 Rolldown 统一依赖预构建和生产打包,开发时底层会走原生 ESM 按需转译并精准热更新。

Rollup 与 Webpack 的区别​

Rollup 基于 ESM 实现高效的 Tree-shaking,适用于工具库的打包;而 Webpack 的代码分割及热更新等功能更为齐全,适用于大型项目的构建。

⭐ 对微前端的理解及解决方案​

微前端就是把一个前端项目拆成多个子应用,各自独立开发和部署,解决多团队并行迭代的问题;可以通过 qiankun 等主流框架接入微前端。

接入微前端的过程有没有遇到什么问题​

在项目中有遇到过微前端的热更新问题,主要表现在子应用的热更新失效,改代码得手动刷新页面才能看到变化。后面定位到是因为微前端沙箱拦截了热更新用的 WebSocket 连接。解决办法是在主应用中调整沙箱配置,在子应用中配置 Access-Control-Allow-Origin 允许跨域,以恢复热更新。

微前端里子应用重复安装 NPM 包怎么优化​

在主应用用 Webpack 5 模块联邦或 externals 把公共依赖提出来共享给子应用,避免每个子应用各打一份。

⭐ 说说微前端的沙箱隔离机制​

微前端中多个子应用共享同一运行环境,容易导致全局变量污染和样式冲突,而沙箱隔离技术可以为每个子应用创建独立的环境,避免互相干扰;像 qiankun 就通过快照沙箱和代理沙箱来隔离 JS 环境,同时通过 Shadow DOM 和作用域沙箱来隔离样式。

快照沙箱和代理沙箱的区别​

快照沙箱是记录子应用加载前的全局状态,在子应用卸载时还原该状态,同时缓存子应用运行期间产生的变更,仅支持单实例运行,无法同时处理多个子应用的状态隔离。而代理沙箱是基于 Proxy,为每个子应用创建独立的全局代理对象,子应用的修改仅作用在代理对象上,支持多实例同时运行,qiankun 就默认使用代理沙箱。

⭐ 微前端各应用间如何通信​

qiankun 中可以通过 initGlobalState 创建全局状态,然后在子应用中通过 onGlobalStateChange 订阅,再通过 setGlobalState 去修改状态。

浏览器丨性能​

浏览器渲染进程有哪些线程​

浏览器渲染进程主要包括 GUI 渲染线程、JS 引擎线程、事件触发线程、定时器线程和异步请求线程这几种,另外还有合成线程和光栅化线程负责出图,所以 transform 动画不卡主线程。

⭐ 浏览器的渲染过程​

浏览器的渲染过程是解析 HTML 和 CSS,分别构建 DOM 树和 CSS 规则树,然后两者结合生成渲染树,接着做布局计算位置,再绘制,最后合成上屏。合成是独立于主线程的,所以只用 transform 和 opacity 的动画不会卡主线程。

渲染过程中遇到 JS 文件会如何处理​

浏览器渲染过程中遇到 JS 会暂停文档的解析,直到 JS 执行完。要优化的话,可以将 script 标签放在后面,也可以给 script 标签设置 defer 或 async 属性。

⭐ script 标签 defer 和 async 的作用​

浏览器渲染过程中遇到 JS 会暂停文档的解析,直到 JS 执行完。如果 script 标签设置了 defer 属性,JS 会并行下载,等文档解析完再按顺序执行;而如果设置了 async,JS 脚本也会并行下载,但下载完会立刻执行,不保证执行顺序。

⭐ 浏览器输入 url 发生了什么​

首先检查缓存,没有则通过 DNS 解析域名;然后三次握手建立 TCP 连接;HTTPS 的话还要先做 TLS 握手;接着浏览器发送 HTTP 请求,拿到响应后开始解析渲染。连接默认会保持复用以减少开销,只有超时或关闭时才会四次挥手断开。

对三次握手的理解​

TCP 三次握手指的是建立连接时,需要在客户端和服务器之间依次发送 SYN、SYN+ACK 和 ACK,以确认双方都能发送和接收,如果只有两次握手,服务端无法确认客户端是否能接收。

对四次挥手的理解​

TCP 四次挥手就是客户端发送 FIN,服务器先回 ACK,表示收到但可能还有数据没发完,等数据发完再发送 FIN 给客户端,客户端收到后回 ACK 确认,连接才真正断开。

如何实现前端自动化性能监控​

通过 PerformanceObserver 采集性能指标,把性能数据推给 SDK,在页面空闲或关闭时批量上报,在后台分析数据,超标告警。

对数据埋点的理解​

埋点就是采集用户行为数据,用来分析用户习惯和转化统计。其中手动埋点最准,能带业务字段,但维护成本高;而全埋点 SDK 自动采集全覆盖,但数据噪音大,一般核心链路还是得靠手动埋点。

⭐ 如何提升页面性能​

首先采集页面关键的性能指标,例如 LCP、INP、CLS 这些,然后根据各个指标,拆分成视觉交互和加载时长两个方向的优化。针对视觉交互,可以从屏幕帧率入手,减少重排和重绘,并利用防抖和节流,减少调用频率;而针对加载时长,可以对资源进行压缩合并,减少 HTTP 请求,配合 CDN 来加速静态资源的载入;其次,充分利用懒加载,可以减少首次加载的资源量,充分利用缓存,可以减少重复请求。

⭐ 常见的性能指标有哪些​

LCP 看主要内容出来快不快,INP 看交互响应速度,CLS 看页面是否稳定。

什么是 BFCache​

BFCache 是浏览器的前进后退缓存,从页面跳走时浏览器会缓存原来的页面,当点击后退时会直接恢复,不用重新加载;可以通过监听 pageshow,根据 persisted 判断页面是不是从 BFCache 恢复的,如果页面监听了 unload 或响应头设置了 no-store,都会导致 BFCache 不生效。

⭐ 首屏加载缓慢原因,如何优化​

首屏加载缓慢有可能是网络延时、资源太大或脚本阻塞等原因导致的;解决方法有对资源进行压缩合并,减少 HTTP 请求,配合 CDN 来加速静态资源的载入;其次,利用 SSR 服务端渲染也可以提升首屏加载速度。

白屏时间是什么,如何计算​

白屏时间指的是从打开页面到屏幕上第一次出现可见内容的时间,对应的是浏览器自带的 FP 指标,也就是首次绘制、包含背景色;FCP 是首次绘制内容、要更晚一点,两者要区分开。

⭐ 重排和重绘的区别​

重排(reflow)指的是元素的几何属性发生变化,导致重新计算布局;而重绘(repaint)指的是元素的视觉属性发生变化,不改变布局;其中,重排一定会触发重绘,性能开销更大。

⭐ 如何避免重排和重绘​

尽量使用 transform 来实现位移、避免频繁修改样式和操作 DOM 等等,都可以避免重排和重绘。

requestAnimationFrame 的作用​

requestAnimationFrame 会在浏览器下次重绘前执行回调函数,可以替代定时器来制作动画,由于动画频率与浏览器刷新频率一致,因此不会出现闪动,可以保证动画的流畅。

高频更新的数据为什么不要放进 state​

明细如果每次都写进组件状态,整棵树就会跟着重渲染。所以我把明细放在组件外面,用 requestAnimationFrame 合到一帧里画,同一项只留最新值。数量少就用 DOM,上千再用 Canvas,再多就用 WebGL 做实例化,这样绘制次数更少。真要进组件,就用 useSyncExternalStore 只订一份汇总,免得一次渲染里读到前后两套数据。

二维地图、三维地球和自绘三维怎么选​

如果只是平面上的位置和路线,我就用 MapLibre 这类矢量地图,点很多再叠一层 WebGL。要地形或者按时间回放,再用 Cesium,并打开 requestRenderMode,没变化就不要每帧重画。点云、框选这种自有三维用 Three.js,但不拿它当底图。另外 Mapbox 从 2.0 起是商业许可,要开源就用 MapLibre。没有三维场景,就不上三维。

定位坐标和底图坐标不一致怎么办​

卫星定位一般是 WGS84,高德和腾讯的底图是 GCJ-02,百度又是另一套。所以混在一起时,点会整段偏离道路。我会在数据进画面之前转成底图那一套,而且只转一次,让实时和回放走同一条转换,不在每个点绘制时现算。公开地图还要做坐标保密处理,所以转换用合规接口,不自己编算法。

大量三维对象怎么点选和旋转才不卡​

重复的物体我用 InstancedMesh 一次画很多个,点集用 Points,而不是每个点一个网格,视锥外面的也先不更新。点选的时候,先用包围盒粗筛,再对剩下的少量做射线检测。坐标则放进类型化数组,这样就不用每帧去遍历普通对象。重计算放到 Worker 里,主线程只留下交互。

⭐ 防抖和节流的区别和使用场景​

防抖(Debounce)是触发后先等待,如果等待期间又触发了则重新计时,适用于搜索联想等场景;而节流(Throttle)是不管触发多频繁,每指定时间内最多执行一次,适合滚动、拖拽等场景。

Web 页面如何实现图片的懒加载​

给 img 标签设置 loading="lazy",浏览器会自动处理基本的懒加载;如果要精细控制,可以创建一个 IntersectionObserver 对象,监听元素是否进入可视区域,从而决定是否加载图片资源。

接口返回大量数据如何优化​

最常见的是通过分页,减少单次请求的数据量;也可以使用虚拟滚动,只渲染可视区域的元素;还可以启用 gzip,对返回的数据进行压缩,以提高传输效率;另外可以通过 Web Worker,将数据处理放在后台线程中进行,这样不会阻塞主线程。

⭐ 对虚拟滚动的理解​

虚拟滚动是一种优化长列表渲染性能的技术,主要是用一个和总数据等高的占位容器撑出完整滚动条,在滚动时根据位置计算出当前需要渲染的数据范围,并复用 DOM 节点替换内容,以模拟完整的滚动列表。

// 定高虚拟滚动
占位层.height = 数据条数 × 每项高度 // 撑出完整滚动条
onScroll(scrollTop) {
start = floor(scrollTop / 每项高度) // 从哪条开始画
end = start + ceil(视口高度 / 每项高度) // 画到哪条
渲染 data[start..end] // 只渲染可视区
渲染层.offsetY = start × 每项高度 // 偏移到正确位置
}

⭐ 不定高的虚拟列表怎么处理​

可以先用预估好的高度占位,渲染完用 ResizeObserver 测量真实高度并回填缓存,然后重算总高度和后续项偏移。

对 Web Worker 的理解​

Web Worker 是浏览器提供的一种多线程技术,可以把一些繁重的任务放到一个独立线程中去跑,这样不会阻塞主线程,处理完的任务通过 postMessage 传回主线程,然后主线程通过 onmessage 事件来接收。Web Worker 虽然不能直接操作 DOM,但用来处理文件导出和音视频解析等这类吃 CPU 的任务很实用。

对 IndexedDB 的理解​

IndexedDB 是一个运行在浏览器上的非关系型数据库,用于在客户端存储大量结构化数据,存储容量由浏览器和磁盘空间决定,支持事务和索引,适合离线应用。

如何实现浏览器标签页之间的通信​

可以使用 WebSocket 来通过服务器中转;也可以使用 BroadcastChannel API 创建一个消息通道,各标签页可以通过该通道发送和接收消息;还可以直接通过 localStorage 来实现通信,当一个标签页修改了 localStorage 时,其他标签页可以通过监听 storage 事件来拿到修改后的数据。

网络丨服务端​

TCP/IP 五层模型是怎样的​

应用层、传输层、网络层、数据链路层、物理层。

TCP 与 UDP 的区别​

TCP 是面向连接的协议,保证数据可靠送达;而 UDP 是无连接的协议,速度快但不可靠,适用于视频直播这种实时性要求高的场景。

TCP 的有哪些机制(TCP 为什么可靠)​

TCP 的机制除了三次握手和四次挥手外,还有重传机制,在数据丢失时重新发送;以及流量控制,防止发送太快;另外还有拥塞控制,避免网络堵塞。

DNS 是什么​

DNS 是域名系统,负责把输入的域名转为 IP 地址。

对 HTTP 的理解​

HTTP 是应用层的超文本传输协议,用于客户端和服务器之间的通信,它是无状态的,每次请求都是独立的。

为什么 HTTP 是无状态的​

HTTP 无状态指的是协议本身不保存之前请求的信息,每个请求都是独立的。这么设计是为了简化通信过程,如果要保存状态,需要用到 Cookie 或 Session。

HTTP 状态码有哪些​

  • 1 开头表示指示信息;

  • 2 开头表示响应成功;

  • 3 开头表示重定向;

  • 4 开头表示客户端错误;

  • 5 开头表示服务端错误。

HTTP 301、302、304 的区别​

301 是永久重定向、302 是临时重定向、304 是请求的资源未发生变化,可以直接复用本地缓存;

HTTP 400、401、403、404 的区别​

400 是请求报文格式错误、401 是未认证、403 是没权限、404 是资源不存在

GET 和 POST 的区别​

GET 的请求参数放在 URL 中,幂等且可以缓存,所谓长度限制其实来自浏览器和服务器的实现,HTTP 规范本身没有规定;而 POST 的请求参数放在请求体中,非幂等且默认不缓存;

⭐ HTTP 与 HTTPS 的区别​

HTTP 明文传输,无需证书,默认端口是 80;而 HTTPS 加了加密层,需要证书验证服务器身份,更为安全,默认端口是 443。

⭐ HTTPS 是如何保证安全的​

HTTPS 在 HTTP 的基础上加了 TLS 加密层,通过混合加密来保证数据隐私,通过摘要算法来保证数据完整,通过数字证书来验证服务器身份。

HTTPS 的加密算法是怎么样的​

HTTPS 用的是混合加密,在握手阶段用非对称加密做身份认证和密钥协商,握手完成后用对称加密传数据。

⭐ HTTP 2.0 有什么升级​

HTTP/2 基于 TCP 协议,支持二进制分帧传输,而且引入了 Header 压缩和多路复用,提升了传输效率,之前还支持服务器推送,不过 Chrome 106 已经移除了这个能力,现在更推荐用 preload 或 103 早期提示来替代。

HTTP 3.0 有什么升级​

HTTP/3 把底层从 TCP 换成基于 UDP 的新传输协议,建立连接更快,而且每个流独立传输,解决了 HTTP/2 在 TCP 层的队头阻塞问题。

对 WebSocket 的理解​

WebSocket 是全双工通信协议,客户端和服务器只需一次握手,就可以创建持久连接,并进行双向通信,适用于实时聊天和股票等场景。

HTTP 与 WebSocket 的区别​

HTTP 是一次请求对应一次响应,而 WebSocket 握手之后双方都能随时发消息。

SSE、WebSocket、WebTransport 怎么选​

服务器单向推送的话选 SSE,双向实时传数据的话选 WebSocket,低延迟音视频的话选 WebTransport。

⭐ AI 对话为什么普遍用 SSE 而不是 WebSocket​

因为 AI 对话是服务器单向推送,用不到 WebSocket 的双向能力,而且 SSE 又是标准 HTTP,网关、负载均衡和鉴权都能复用,运维成本更低。

⭐ WebSocket 断线重连要注意什么​

WebSocket 断线不能断了就立刻连,要用指数退避加随机抖动重连,避免服务重启后所有人同时重连造成雪崩。连上之后还要同步状态,把断线期间的增量补回来。

持续推送的消息怎么保证不乱​

每条消息都带序号和服务端时间。如果处理不过来,就丢掉中间消息,只留最新状态,不要排队把旧的画出来。序号要是断了,能补就补,补不了就重新拉一份全量。断线时用指数退避加随机抖动,避免所有客户端一起重连。连上以后,先对齐全量,再接增量。如果只是接收,用 SSE 就够;还要下发命令,再用 WebSocket。

⭐ 前端怎么消费大模型的流式输出​

一般用 fetch 发 POST 请求,从 response.body 拿到 ReadableStream 响应流,配合 TextDecoder 解码器边读边解码;而 SSE 每帧以空行结尾,所以要维护缓冲区,按空行切出完整帧再解析;如果需要中断,可通过 AbortController 中止请求。

⭐ 大模型流式输出中途断了怎么办​

AI 对话一般用 fetch 消费流,没有 EventSource 那种自动重连,所以得自己兜:在业务层记录已经收到的断点位置,重连时带给服务端,从断点接着推,避免重新跑一遍模型;同时要限制重试次数、失败要有明确提示。

流式 Markdown 渲染怎么防止截断和闪烁​

流式输出的 Markdown 随时会停在半截,比如代码块只开了没闭合,直接渲染就会乱。做法是分两块:已经结构完整、不会再变的内容放稳定区,正常渲染、冻结缓存;尾部还没闭合的部分放草稿区,先用容错的方式渲染成纯文本,等它闭合了再挪进稳定区。更新粒度按块来,不要每来一个 token 就整篇重新解析,否则会一直闪。代码块也是同理:没闭合之前先不高亮,闭合了再上高亮。

前端如何取消进行中的请求​

可以先创建 AbortController 对象,然后把 signal 取消信号传给 fetch 或 Axios,在需要取消时调用 AbortController 的 abort 方法取消请求。

对 CDN 的理解​

CDN 是内容分发网络,把静态资源缓存到离用户最近的节点上,以降低延迟。当用户访问 CDN 地址时,会通过 DNS 调度分到最近节点,节点有缓存则直接返回,没有则回源站拉取并缓存。

Axios 的原理​

Axios 底层是适配器架构:浏览器端默认用 XMLHttpRequest,Node 端用 http 模块,1.7 之后还加了 fetch 适配器。请求派发前依次执行请求拦截器,然后适配器根据配置发起请求,响应回来再依次执行响应拦截器,最终返回 Promise。

对 Fetch 的理解​

Fetch 是基于 Promise 的请求 API,比 XHR 写法更友好,而且响应体走数据流,可以分块读取;但只对网络请求报错,服务器返回 400 或 500 时会当作成功的请求。

⭐ 大文件上传怎么做​

先用 file.slice() 把文件切片,然后计算文件的哈希,看服务端能不能秒传,不能就只传缺失的部分做断点续传,全部传完之后在服务端进行合并。

对同源策略和跨域的理解​

同源策略是浏览器的一个安全机制,当协议、域名和端口都相同时就是同源,反之就是跨域。

⭐ 如何解决跨域​

可以通过代理来解决,开发用本地代理、生产用网关转发,让请求变同源;也可以通过 CORS 跨域资源共享,服务端在响应头放行,从而解决跨域。

对代理的理解​

代理用于接收和转发请求,包括正向代理和反向代理。正向代理代理的是客户端,服务器不知道真实用户,例如 VPN 就是正向代理;而反向代理代理的是服务器,用户不知道自己访问的是代理服务器,例如 Nginx 可以用来做反向代理。

Node.js 反向代理具体怎么配​

可以通过原生的 http-proxy 或 express 的 http-proxy-middleware 中间件配置代理路径和目标地址,将请求转发到后端服务并处理跨域,实现反向代理。

对 Nginx 的理解​

Nginx 是一个高性能的 HTTP 服务器,常用于反向代理和负载均衡,前端项目中 Nginx 还负责托管静态资源。

BFF 是什么,价值边界在哪​

BFF 是由前端团队自己维护的一套 Node 服务,负责把零散接口拼接成页面想要的,以及做字段裁剪和格式适配等操作,不碰业务规则和持久化状态。

对 CORS 的理解​

CORS 即跨域资源共享,用来解决跨域问题。简单请求要同时满足:方法是 GET、HEAD 或 POST,请求头只带几个安全字段,而且 Content-Type 只能是 form-urlencoded、multipart-form-data 或 text-plain——所以带 application/json 的请求也算复杂请求,会触发预检。简单请求可直接给响应头设置 Access-Control-Allow-Origin 来放行来源;而剩余的复杂请求会先触发 OPTIONS 预检,需要通过了才发正式请求。

⭐ CSRF 攻击原理与防御措施​

CSRF 即跨站请求伪造,主要是诱导用户访问危险网站,然后利用用户的登录状态来冒充用户发送危险请求;防御措施有使用 CSRF Token 验证、检验 Origin 或 Referer、以及 Cookie 设置 Samesite 属性来限制第三方 Cookie 等等。

⭐ XSS 攻击原理与防御措施​

XSS 即跨站脚本攻击,通过注入恶意脚本在浏览器中执行。具体包括注入数据库的存储型 XSS 攻击、注入 URL 的反射型 XSS 攻击以及注入前端行为的 DOM 型 XSS 攻击;防御措施有对脚本进行转义、使用 CSP 建立资源白名单、以及 Cookie 设置 http-only 属性等等。

Cookie 是服务器下发给浏览器的一小块数据,它会在浏览器下次发送请求时带上,用来给无状态的 HTTP 补上登录态等状态。缺点是存储空间有限,通常只有 4k 左右;而且每次发送请求时携带 Cookie,会增加请求开销;另外 Cookie 不设置 HttpOnly 的话还可能被 XSS 读取。

Cookie 可能被 XSS 或 CSRF 攻击。可以设置 HttpOnly 属性,防止 Cookie 被 JS 读取;也可以设置 SameSite 属性为 Strict 或 Lax,限制 Cookie 的跨站请求;还可以设置 Secure 属性,使 Cookie 只能通过 HTTPS 协议传输。以此来保证 Cookie 的安全性。

SameSite 包括 Strict、Lax 和 None 这三个值,他们的区别在于跨站请求带不带 Cookie;其中 Strict 一律不带,而 Lax 只放行顶层 GET,None 的话则全部带上,但必须配 Secure;

对 Session 的理解​

Session 保存在服务器,用于存储用户的会话状态。当浏览器首次请求时,服务器会根据提交的信息创建 Session,并生成唯一的 Session ID,通过 Cookie 发送给浏览器,浏览器在后续请求中会携带该 Session ID,服务器根据这个 Session ID 查找对应的 Session,从而获取用户的会话状态。

Cookie 保存在客户端,存储空间有限,通常只有 4k 左右,而且存在安全隐患;而 Session 保存在服务端,存储空间更大,也更安全。

SessionStorage 和 LocalStorage 是什么​

SessionStorage 只在当前标签页有效,关闭就清空;而 LocalStorage 可以在同个网站的多个标签页间共享,会持久保存,除非手动清除。

⭐ 浏览器的缓存机制是什么​

浏览器的缓存机制是将请求的资源存储到本地,供下次访问使用,以提高加载速度;主要包括强缓存和协商缓存。

⭐ 强缓存和协商缓存的区别​

强缓存指的是浏览器不向服务器发送请求,直接从缓存中读取资源;而协商缓存指的是强缓存失效后,浏览器向服务器发送请求,用携带的资源标识让服务器判断有没有更新,如果没变就返回 304,直接复用本地缓存。

什么是 ETag​

ETag 是服务器为每个资源生成的唯一标识,客户端在下次请求时通过 If-None-Match 携带,服务器根据 ETag 来判断资源是否发生变化,如果没变则返回 304,告知客户端可以直接复用本地缓存。

Last-Modified 和 ETag 的区别​

Last-Modified 看资源最后修改时间,开销小但只精确到秒,容易误判;ETag 按内容生成唯一标识,能更精确判断变化,优先级也更高。协商缓存时两者都可能出现,ETag 更可靠。

Redis 缓存穿透、击穿、雪崩的区别与解决方案​

这三个是不同的问题:穿透是查的数据压根不存在,缓存和库里都没有,请求每次都落到库上,解法是缓存空值加短过期时间,或者上布隆过滤器;击穿是单个热点 key 到期的瞬间,大量请求同时打到库上,解法是加互斥锁只放一个请求去查,或者干脆逻辑过期、后台异步续;雪崩是大批 key 同时失效,或者 Redis 整个挂了,解法是过期时间加随机值打散,再加多级缓存和熔断兜底。

如何设计一个登录业务​

首先在登录密码方面,使用慢哈希算法加独立随机盐存进数据库;在登录态方面,走 Session + Cookie 的形式,并且设置好 Secure 属性,只走 HTTPS,同时设置 HttpOnly 和 SameSite 属性,防止 XSS 和 CSRF 攻击;在风控方面,通过限制失败次数和触发验证码等形式提升安全性。

前端权限只做隐藏为什么不够​

只把按钮藏起来是不够的,因为请求还是能直接发出去。菜单和路由决定进不进得去,按钮决定看不看得到,至于能看到哪些数据,必须由服务端按身份过滤。所以前端的隐藏和禁用只是体验,真正的拒绝要落在接口上。权限也要在进入页面时重新获取,不要写死在前端包里。

扫码登录是怎么实现的​

PC 上先请求生成一个二维码,二维码里其实就是一个一次性的临时 ticket,很短命。手机扫码,扫的就是这个 ticket;如果手机上已经有登录态,会弹一个确认,问用户要不要登录。用户确认后,服务端把 ticket 和账号绑定,给 PC 端签发一个一次性凭证。PC 这边一般用短轮询问「这个 ticket 好了没」,好了就拿凭证去换正式登录态。整个过程二维码有明确的状态流转:待扫描、已扫描待确认、已确认、已过期,这样也能防住过期被复用和串号。

⭐ 什么是 JWT?如何实现无状态认证​

JWT 是服务端签发的身份令牌,包含用户信息和过期时间,客户端每次请求会带上,服务端核对通过即可认证,无需保存 Session,从而实现无状态认证。

AK/SK 签名鉴权和用户登录态有什么区别​

AK 是身份标识,会随请求发出去,SK 是密码,只留在本地;他们会给请求计算签名,对上了就说明请求合法;而用户登录态是登录后发的凭证,之后的请求会带上,用来表示用户已登录。

⭐ 什么是双 Token?如何实现无感刷新​

我理解的双 Token 就是将登录令牌拆为两种,一种是存到内存的临时凭证,例如十五分钟就失效,专门在请求头中做认证;另一种是存到 HttpOnly Cookie 的长期凭证,临时凭证失效时会拿它去换新的,这样即使临时凭证被盗,别人也无法长期使用;而前端要做的就是在响应拦截器中兜住 401,拿到新的临时凭证再重新请求,让用户感知不到登录过期。

401 并发请求同时到达,怎么避免重复刷新​

可以通过 Promise 配合 await,让第一个 401 负责换新的临时凭证,后面的 401 则先排队;等新凭证拿到后,排队中的请求再带上新凭证挨个重发,让用户感知不到中断;如果新的临时凭证换取失败,则直接返回登录页。

⭐ 什么是单点登录,前端如何实现单点登录​

单点登录(SSO)即用户只在统一认证中心登录一次,就能访问多个子系统,如果未登录就重定向到认证中心,登录成功后带着授权码跳回,后端拿授权码换取令牌,然后给子系统验证通过,实现单点登录。

Koa 和 Express 的区别​

Koa 比 Express 更轻量,但路由等功能要自己装中间件;在中间件执行方面,Koa 使用洋葱圈模型的函数调用栈执行中间件,await next() 之后还能回来做收尾,而 Express 使用线性回调链执行中间件,一件件往下传。

描述 koa 洋葱圈模型​

Koa 的洋葱圈模型是将中间件像洋葱一样分层,通过 await next() 把请求往里传;最内层处理完后,再按相反顺序回到外层。

设计模式丨算法丨 AI​

JS 有哪些设计模式​

单例模式、观察者模式、工厂模式、适配器模式、装饰器模式、迭代器模式、代理模式。

描述单例模式​

单例模式指的是确保一个类只有一个实例,避免创建多个实例造成资源浪费。

什么是 RAG?完整链路是怎样的​

RAG 就是把检索和生成拼起来:先把文档解析出来切成小块,每块做 embedding 存进向量库;用户提问时,把问题也转成向量去检索,召回相关的块,再用 rerank 重排取 TopK,拼进 prompt 让模型基于这些内容作答。相比微调,它的好处是知识更新不用重训,还能给出引用出处。我这边做的是知识库前端那一段,负责切片的展示和管理交互,检索和 embedding 是服务端跟算法侧做的,这个边界我会说清楚。

Function Calling 的执行流程是怎样的​

先把工具清单按 JSON Schema 描述给模型;用户提问后,模型不直接回答,而是返回它想调用的函数名和参数;真正执行函数的是我们应用自己,不是模型;执行完把结果以 role 是 tool、带上对应的 tool_call_id 回填给模型,模型再基于结果生成最终回答。如果还需要调别的工具,就再循环一轮。一句话概括:模型只负责决定调什么,执行和回填都是应用的事。

什么是 Agent,和 workflow 有什么区别​

Agent 是大模型自己决定下一步做什么;而 workflow 是开发者提前写好流程,模型只在固定步骤中推理和执行。

Agent 的记忆怎么设计,上下文超限了怎么办​

记忆一般分两层:短期记忆就是当前对话,放在上下文里;长期记忆是跨会话的,存到向量库或数据库里,需要的时候检索回来。上下文快超限的时候,常见几步:先把最老的、用不上的工具输出裁掉;还不够就把历史做摘要压缩,用摘要重启一个新窗口;再不行就只带最相关的几条历史,按需检索,而不是全塞进去。设计上就是存、取、更新三件事:什么值得存、怎么召回、过期的怎么合并或者忘掉。另外有个经验是,关键信息别埋在中间,模型对开头和结尾更敏感。

什么是 MCP​

MCP 即模型上下文协议,可以理解成 AI 应用的 USB 接口:工具方按协议实现一次,各个 AI 应用都能接。它有三个核心原语:tools、resources、prompts——tools 是模型能调用的操作,resources 是给应用读的数据,prompts 是给用户用的模板;控制层级也一一对应:tools 由模型决定,resources 由应用决定,prompts 由用户触发。传输上,本地一般走 stdio,远程走 Streamable HTTP。

MCP 和 Function Calling 有什么区别​

这两个不是一个层级的东西:Function Calling 是模型自己的能力,让模型能决定调哪个函数、传什么参数;MCP 是一套接入协议,解决的是工具怎么接进来的问题。没有 MCP 之前,同一个工具换个 AI 应用就要重新适配一遍;有了 MCP,工具方按协议实现一次,谁都能接。实际跑的时候两者是配合关系:MCP 服务端暴露的工具,会被转成模型能认的工具格式,还是由模型决定调不调、应用负责执行。

常用的 Prompt 技巧有哪些​

我一般先进行角色设定,然后给一两个示例,让 AI 一步步推理,接着定好输出格式,并进行任务拆解分批执行,确保结果可靠。

你平时怎么用 AI 写代码​

一些简单的页面、表单、脚本等体力活我会放心交给 AI 去写;然后涉及到业务逻辑改动或重构的,我会先定方向,再让 AI 出方案,补充各种具体细节后落地执行;而涉及到权限、核心功能的我会更为谨慎,提交前全部过一遍,CR 时重点看边界条件和错误处理,把控好质量。

⭐ 你怎么保证 AI 生成代码的质量​

在开发前通过规则文件定好规范,例如定好架构约束和代码生成的边界条件;在开发时做好类型和规范检查,以及对应的测试,测试通过才合代码,合之前还要全部过一遍,CR 时重点看边界条件和错误处理,把控好质量。

AI 生成的代码踩过什么坑​

AI 有时会重复造轮子,它不看项目中已有的封装,而是自己重写一套;看着能跑,但不完全一致,后面如果要改逻辑或样式,得多个组件一起改,维护成本更高;所以我现在 CR 时会看有没有重复造轮子,有的话的一律打回。

AI 能写 80% 的代码,前端的价值在哪​

AI 把写代码的成本打下来了,现在更重要的是我们来判断哪些能做、哪些不能做、能不能这么写、可不可以上线、以及出事了谁来扛。所以现在开发要能懂业务、能把控风险,以及能做好架构取舍和质量把关,确保 AI 的产出能顺利交付。总之就是 AI 管生成,我们管方向和质量兜底。

给你一段 AI 生成的代码,怎么判断能不能上线?​

我一般会先看业务的实现是否符合预期,尤其是一些复杂业务或核心功能;然后再看 AI 容易忽略的点,比如副作用有没有清理,空状态或失败的情况有没有处理好,有没有越界或不必要的改动,以及整体实现是否符合团队规范等;最后跑边界和失败的场景测试,最好是能达到我能讲清楚每一段变更为什么这么去写,确认完全没问题再上线。

AI 对话中的「停止生成」怎么实现?​

用户点停止,可以通过 AbortController 去中断当前的流式请求,已经输出的内容可以留着;另外,主动停止不能当做网络错误去处理,服务端要跟着停止,否则会继续烧 token。