Account 简答类
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 的理解,有什么缺点
Cookie 是服务器下发给浏览器的一小块数据,它会在浏览器下次发送请求时带上,用来给无状态的 HTTP 补上登录态等状态。缺点是存储空间有限,通常只有 4k 左右;而且每次发送请求时携带 Cookie,会增加请求开销;另外 Cookie 不设置 HttpOnly 的话还可能被 XSS 读取。
Cookie 有什么安全问题,如何解决
Cookie 可能被 XSS 或 CSRF 攻击。可以设置 HttpOnly 属性,防止 Cookie 被 JS 读取;也可以设置 SameSite 属性为 Strict 或 Lax,限制 Cookie 的跨站请求;还可以设置 Secure 属性,使 Cookie 只能通过 HTTPS 协议传输。以此来保证 Cookie 的安全性。
Cookie 的 SameSite 有哪几个值
SameSite 包括 Strict、Lax 和 None 这三个值,他们的区别在于跨站请求带不带 Cookie;其中 Strict 一律不带,而 Lax 只放行顶层 GET,None 的话则全部带上,但必须配 Secure;
对 Session 的理解
Session 保存在服务器,用于存储用户的会话状态。当浏览器首次请求时,服务器会根据提交的信息创建 Session,并生成唯一的 Session ID,通过 Cookie 发送给浏览器,浏览器在后续请求中会携带该 Session ID,服务器根据这个 Session ID 查找对应的 Session,从而获取用户的会话状态。
Cookie 和 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。