前端懂王面试题
1. React
1.1 Hooks
React Hooks 是 React 16.8 版本引入的一项革命性特性,它允许你在函数组件中使用状态、生命周期和其他 React 特性,而无需编写类组件。
使用 Hooks 的黄金规则:
- 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks,这可以保证 Hook 在每次渲染时按相同顺序执行,让 React 正确保存内部状态。
- 底层原理:基于调用顺序的“链表”。
- 组件的hooks是用链表这种数据结构来进行连接的,通过next属性保持执行顺序。如果中间的断开,会导致后面的钩子找不到。
- 只在 React 函数组件或自定义 Hook 中调用 Hooks:不要在普通 JavaScript 函数中使用。
1.1.1 useTransition
useTransition 是 React 18 引入的一个 Hook,用于将某些状态更新标记为“过渡更新”,从而允许更紧急的更新(如用户输入)优先渲染,避免界面卡顿,提升交互体验。
核心作用
当你在应用中执行一个可能耗时较长的状态更新(例如切换页面、筛选大量数据)时,UI 可能会暂时无响应,导致用户感觉卡顿。
useTransition允许你将这些更新标记为“低优先级”,让 React 先处理更紧急的更新(如输入框的字符变化),稍后再处理过渡更新,并在此期间显示一个加载指示器。返回值
javascriptconst [isPending, startTransition] = useTransition();isPending:布尔值,表示过渡更新是否正在进行中。你可以用它来显示一个加载提示(如旋转器)。startTransition:一个函数,接受一个回调函数。将回调内的状态更新放入“过渡”中执行,这些更新会被视为低优先级。
基本用法
javascriptimport { useTransition, useState } from 'react'; function SearchResults() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; // 紧急更新:立即更新输入框的值 setQuery(value); // 将耗时的搜索结果更新标记为过渡 startTransition(() => { const filtered = expensiveFilter(value); // 假设这是一个耗时操作 setResults(filtered); }); }; return ( <div> <input value={query} onChange={handleChange} /> {isPending && <div>Loading results...</div>} <ul>{results.map(item => <li key={item}>{item}</li>)}</ul> </div> ); }主要特点与注意事项
- 必须在
startTransition内部调用状态更新函数:只有包裹在startTransition中的setState才会被视为过渡。 - 过渡可以被用户交互中断:如果用户再次触发新的输入,React 会放弃旧的过渡更新,转而处理新的紧急更新。
- 与
useDeferredValue的区别:useTransition给你一个手动包裹状态更新的函数,适用于你控制状态更新的时机。useDeferredValue返回一个“延迟版”的值,适合处理由 props 传入或无法直接控制更新的数据。
- 只在并发模式下有效:虽然 React 18+ 默认支持并发特性,但如果你强行同步渲染(例如使用旧版
ReactDOM.render),过渡可能退化为同步更新。
- 必须在
1.1.2 useDeferredValue
useDeferredValue 是 React 18 引入的一个 Hook,用于将某个值的更新“延迟”,让 React 优先处理更紧急的更新,从而保持界面流畅。
核心作用
当你有某个不紧急的值(它依赖于用户输入或频繁变化的数据),而计算这个新值又比较耗时(如过滤/排序大列表),你希望:
- 立即响应用户输入等紧急任务(输入框、按钮点击)
- 耗时计算可以“稍后”再做,在这期间 UI 使用旧值或显示加载状态
useDeferredValue会返回一个延迟版本的值:React 会先让紧急更新推进,如果有空闲时间,才会用新值去更新延迟版本,并触发相应的重渲染。基本用法
javascriptimport { useDeferredValue, useState, useMemo } from 'react'; function SearchPage() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); // 延迟版本 // 只有 deferredQuery 变化时才执行代价高昂的过滤 const list = useMemo(() => { return expensiveFilter(deferredQuery); }, [deferredQuery]); const handleChange = (e) => setQuery(e.target.value); // 可选:判断是否还在等待延迟更新 const isStale = query !== deferredQuery; return ( <> <input value={query} onChange={handleChange} /> {isStale && <div>Loading...</div>} <List items={list} /> </> ); }底层原理与注意点
useDeferredValue在底层也会启动一个过渡更新,是startTransition的便捷封装。- 必须配合
React.memo或useMemo使用才能体现性能优势,避免无意义的渲染。 - 仅在 React 18+ 并发模式下有效;如果降级到同步渲染,它退化为同步更新(但仍然会影响顺序,但不会延迟)。
- 不要用于必需立即反映的 UI(如输入框的字符显示),否则用户会感到滞后。
1.1.3 forwardRefs
forwardRef 是 React 提供的一个工具,用于解决函数组件无法直接接收 ref 属性的问题,允许你将父组件创建的 ref 传递给子组件内部的某个 DOM 元素或类组件,从而在父组件中直接操作子组件的具体节点或实例。
forwardRef 只是一个“透传机制”,不会改变组件的行为,仅仅让 ref 可以被函数组件捕获。
1.1.4 useImperativeHandle
useImperativeHandle 是 React 提供的一个 Hook,与 forwardRef 配合使用,用于自定义通过 ref 暴露给父组件的实例值(通常是一组方法)。它让你可以限制父组件对子组件内部 DOM 或状态的访问权限,只暴露必要的 API,而不是整个节点。
配合 forwardRef 使用示例:
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
// 子组件:自定义暴露给父组件的 API
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 定义父组件可以通过 ref 调用的方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
},
getValue: () => inputRef.current.value,
}));
return <input ref={inputRef} {...props} />;
});
// 父组件
function Parent() {
const childRef = useRef(null);
const handleFocus = () => {
childRef.current.focus(); // 调用子组件暴露的 focus 方法
};
const handleClear = () => {
childRef.current.clear();
};
const handleGet = () => {
alert(childRef.current.getValue());
};
return (
<div>
<CustomInput ref={childRef} placeholder="输入内容..." />
<button onClick={handleFocus}>聚焦</button>
<button onClick={handleClear}>清空</button>
<button onClick={handleGet}>获取值</button>
</div>
);
}常见使用场景
- 自定义表单控件:暴露
reset()、validate()、getValue()等方法。 - 动画组件:暴露
play()、pause()、seekTo()等。 - 第三方库封装:将命令式 API(如地图、播放器)封装成可控的 React 组件。
1.1.5 useId
useId 是 React 18 引入的一个 Hook,用于在客户端和服务端生成稳定、唯一且可预测的 ID 字符串,主要解决无障碍属性(如 aria-describedby、htmlFor)和需要唯一标识的 DOM 元素之间的 ID 冲突问题。
- 核心作用
- 生成唯一的 ID:每次调用
useId都会返回一个在该组件树中全局唯一的字符串 ID,例如:r1:。 - 保证服务端渲染(SSR)与客户端水合(Hydration)一致性:在 SSR 场景下,服务端生成的 ID 与客户端首次渲染生成的 ID 完全相同,避免水合不匹配。
- 避免 ID 冲突:即使同一个组件在页面上使用多次,每个实例的 ID 都不同,无需手动拼接前缀或维护计数器。
- 生成唯一的 ID:每次调用
- 使用场景
- 表单输入与标签关联:
<label htmlFor={id}>和<input id={id}> - ARIA 关系属性:
aria-labelledby、aria-describedby需要引用一个或多个元素 ID。 - 无障碍组件库:为内部元素(如弹出层、Tab、组合框)生成稳定 ID。
- 需要唯一标识但不想引入额外状态:比如 SVG 的
<defs>中的渐变或滤镜 id,或list属性与<datalist>的关联。
- 表单输入与标签关联:
- 注意事项
- 与 useMemo/useCallback 无关:
useId不依赖组件的渲染状态,每次渲染返回相同的 ID(前提是组件实例没有卸载重新创建)。 useId返回的 ID 可能包含冒号(:),通常不建议直接在 CSS 中使用这些 ID 作为选择器,应通过 className 或 data 属性替代。- 不适用于列表的 key:生成列表项的
key应该使用数据中的稳定唯一标识(如数据库 ID),而不是useId。因为useId在渲染过程中生成,不支持列表顺序变化时的稳定 key 语义。
- 与 useMemo/useCallback 无关:
1.1.6 useSyncExternalStore
useSyncExternalStore 是 React 18 引入的一个 Hook,用于订阅外部 store(例如 Redux、Zustand、浏览器 API 或任何非 React 管理的状态源),并确保在 store 变化时组件能够同步获取最新快照,同时避免“tearing”问题(UI 不一致)。通过强制同步读取快照 + 受控的订阅通知,彻底解决了撕裂问题。
核心作用
- 同步读取外部 store:在并发渲染(Concurrent Rendering)下,保证组件看到的 store 状态始终一致。
- 避免撕裂(Tearing):当 React 渲染被中断或交织时,不同组件可能读到不同版本的 store 状态,
useSyncExternalStore强制使用同一个快照。
基本用法
javascriptconst snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)参数
subscribe:一个函数,接收一个callback参数。当 store 变化时,必须调用这个callback来通知 React 重新读取快照。返回一个取消订阅的函数。getSnapshot:一个同步函数,返回 store 的当前快照(任意值)。如果两次调用返回值不同(使用Object.is比较),React 会重新渲染组件。getServerSnapshot(可选):在服务端渲染(SSR)或 hydration 时使用,返回初始快照。
返回值
- store 的当前快照(与
getSnapshot返回值一致)。
简单示例:订阅浏览器 API
javascriptimport { useSyncExternalStore } from 'react'; // 订阅 window.innerWidth function useWindowWidth() { const getSnapshot = () => window.innerWidth; const subscribe = (callback) => { window.addEventListener('resize', callback); return () => window.removeEventListener('resize', callback); }; const getServerSnapshot = () => 0; // SSR 时的初始值 const width = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); return width; } function WindowWidthComponent() { const width = useWindowWidth(); return <div>窗口宽度: {width}px</div>; }
1.1.7 useEffect 返回函数执行时机
useEffect 的返回函数(通常称为清理函数)会在以下时机执行:
组件卸载时(一定会执行)
无论依赖项是什么,只要组件从 DOM 中移除,清理函数就会执行。这用于取消订阅、清除定时器、取消网络请求等“收尾”工作。
下一次 useEffect 执行之前(针对依赖项变化的情况)
当组件重新渲染,且
useEffect的依赖项发生变化,React 会先执行上一次 effect 的清理函数,然后再执行新的 effect。- 依赖项为空数组
[]:没有依赖项变化,所以清理函数只会在组件卸载时执行,不会在每次重渲染后执行。 - 依赖项变化:例如
[count]中的count改变,React 会在下一次 effect 运行前调用清理函数,清除上一次 effect 残留的副作用。 - 无依赖:每次 effect 重新执行前。
- 依赖项为空数组
开发环境下的额外执行(严格模式)
在 React 严格模式(
<StrictMode>)下,为了帮助发现意外的副作用,React 会在组件挂载后立即额外执行一次“模拟的卸载和重挂载”。这会使得清理函数在挂载后就被调用一次(紧接着 effect 函数再次执行)。这只发生在开发环境,不影响生产环境。
1.1.8 useEffect 和 useLayoutEffect 的区别
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 浏览器绘制之后(异步) | 浏览器绘制之前(同步) |
| 是否阻塞渲染 | 不阻塞,用户可能先看到旧内容再更新 | 会阻塞渲染,直到 effect 执行完毕 |
| 使用场景 | 绝大多数副作用:数据请求、订阅、日志、非 DOM 修改 | 需要同步测量/修改 DOM 的场景(避免闪烁) |
| 服务端渲染(SSR) | 正常执行(但在 SSR 中不会真正运行,由客户端水合后执行) | 会发出警告,因为服务端没有布局概念,需转为 useEffect 或仅在客户端执行 |
执行时机与渲染流程(React 更新阶段顺序):
- 触发更新(state/props 变化)
- 渲染组件(调用函数组件),得到虚拟 DOM
- React 更新真实 DOM(此时 DOM 已改变,但浏览器还未绘制屏幕)
useLayoutEffect同步执行(此时浏览器尚未绘制,可以读取/修改 DOM 布局)- 浏览器绘制屏幕(用户看到新界面)
useEffect异步执行(不会阻塞绘制)
简单记:
useLayoutEffect发生在 DOM 更新之后、浏览器绘制之前;useEffect发生在绘制之后。对性能与用户体验的影响
useEffect:副作用不会延迟屏幕更新,因此性能更好,UI 响应更快。但用户可能会看到“内容闪动”(例如先显示未格式化的样式,然后立即被修正)。useLayoutEffect:会在绘制前同步执行,如果内部计算量过大,会阻塞渲染导致页面卡顿。但可以避免视觉上的“跳动”或“闪烁”。
1.2 React 严格模式
React 严格模式是一个仅在开发环境下生效的工具,用于帮助你提前发现应用中潜在的 Bug,并让你的代码为未来的 React 版本做好准备。
它与普通的 <div> 或组件不同,<StrictMode> 本身不会渲染任何可见的用户界面。它通过额外执行一系列检查和操作来为你提供警告。所有严格模式的检查都只在开发模式下运行,完全不会影响你的生产环境构建。
React 严格模式主要帮你检查以下几类问题,以便在代码上线前及时修复:
- 识别不安全的生命周期方法:一些旧版的生命周期方法(如
componentWillMount)在异步渲染下使用是不安全的。在严格模式下,如果代码中使用了这些方法,控制台会打印出警告,提醒你使用更安全的替代方案(如添加UNSAFE_前缀)或重构代码。这能确保应用在未来的 React 版本中依然能稳定运行。 - 警告过时或已弃用的 API 使用:
- 过时的字符串
refAPI:会警告你使用已过时的字符串形式ref,并推荐你使用现代且更安全的回调函数ref或createRefAPI。 - 废弃的
findDOMNode方法:会提醒你避免使用该方法,因为它破坏了组件的抽象性,且在现代 React 中使用场景非常有限,更推荐直接使用ref绑定到 DOM 节点。 - 过时的 Context API:会检查并警告你避免使用已淘汰的老旧 Context API。
- 过时的字符串
- 检测意外的副作用,“双重调用”:这是严格模式最核心,也最容易让开发者感到“异常”的行为。为了帮助你发现组件中不纯的渲染逻辑或不规范的副作用代码(例如在
constructor、render函数、useState的初始化函数中执行了副作用操作),React 在开发环境下会故意重复调用以下函数,它的目的是放大和暴露那些依赖于单次调用的代码缺陷:- 类组件的
constructor、render、shouldComponentUpdate等方法。 - 函数组件的函数体。
setState的更新函数(第一个参数)。- 任何被传入
useState、useMemo、useReducer的函数。
- 类组件的
1.3 React 和 ReactDom 的作用
react包:核心库,负责定义组件、管理状态、处理生命周期、协调虚拟 DOM(即reconciler的核心逻辑)。它是平台无关的,无论是 Web、移动端(React Native)还是 VR(React 360)都共用这一套核心。react-dom包:Web 平台的渲染器,负责将react生成的虚拟 DOM 渲染成真实的浏览器 DOM,并提供与浏览器环境相关的 API(例如createRoot、hydrate、findDOMNode等)。
一句话总结:react 负责“想好 UI 长什么样”,react-dom 负责“把想好的 UI 真实画到浏览器上”。
1.4 React 并发更新
React 并发更新是 React 18 引入的一套“非阻塞、可中断”的渲染机制,它让 React 能够根据任务优先级动态调度(中断、暂停、恢复或放弃渲染工作),优先响应用户输入,保证用户交互始终保持流畅,同时充分利用空闲时间执行后台更新。
传统渲染 vs. 并发渲染
特性 传统同步渲染(React 17 及之前) 并发渲染(React 18+) 渲染过程 一旦开始,不可中断,持续到完成 可中断、可恢复,可让出控制权 对用户输入的影响 长任务会阻塞主线程,导致输入卡顿 高优先级任务(如输入)可打断低优先级任务 UI 响应性 可能掉帧或延迟反馈 始终保持高响应性 内部机制 递归调用栈(同步) Fiber 架构 + 时间切片 + 优先级调度 并发更新的核心原理
Fiber 架构:React 16 引入 Fiber 节点(链表结构),将渲染工作拆分为多个小单元。Fiber 可以记录工作进度,使得 React 在执行完一个单元后可以检查是否有更高优先级的任务,如果有,则暂停当前工作,让出主线程。
时间切片(Time Slicing):React 将长时间渲染任务切割成多个小片段,每个片段执行后主动检查是否需要让出控制权(如浏览器是否还有更高优先级任务)。这避免了长任务长期占用主线程。
优先级调度:不同类型的更新被赋予不同优先级:
- 离散事件(点击、键盘输入)→ 最高优先级(立即响应)
- 连续事件(拖动、滚动)→ 稍高优先级
- 过渡更新(切换页面、搜索过滤)→ 低优先级
- 数据获取(Suspense) → 可进一步降级
React 的调度器(Scheduler)根据优先级决定先执行哪些更新。
如何在 React 18 中使用并发特性
你将根组件渲染方式从
ReactDOM.render改为createRoot,即开启并发模式:javascript// 旧方式(同步模式) import ReactDOM from 'react-dom'; ReactDOM.render(<App />, document.getElementById('root')); // 新方式(并发模式) import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render(<App />);浏览器空闲时间
原理主要依赖
requestIdleCallback和 React 自己实现的 Scheduler 调度器。原生 API:
requestIdleCallback浏览器提供了一个原生方法
window.requestIdleCallback,它允许你传入一个回调函数,该函数会在浏览器主线程空闲时执行。回调函数会收到一个参数,其中包含一个timeRemaining()方法,表示该空闲时段还剩多少毫秒。javascriptrequestIdleCallback((deadline) => { // 有空闲时间时,执行任务 while (deadline.timeRemaining() > 0 && hasMoreWork) { performUnitOfWork(); // 执行一个渲染工作单元 } // 如果还有剩余任务,再次注册空闲回调 if (hasMoreWork) requestIdleCallback(performWork); });React 内部使用类似机制将大任务拆分成多个小单元(Fiber 节点),并在每个单元执行后检查是否还有剩余时间,若有则继续执行下一个,否则让出控制权,等待下次空闲。
React 的自定义调度器(Scheduler)
由于
requestIdleCallback在不同浏览器的支持度、触发频率和优先级控制上存在局限(例如在 Safari 下支持不佳,且触发频率约为 20-50ms,不够精细),React 从 16 版本开始自己实现了一个轻量级的 Scheduler 包。Scheduler 的核心原理:
- 模拟
requestIdleCallback行为:利用MessageChannel+postMessage或setTimeout在宏任务队列中插入任务,以获取主线程空闲时机。 - 时间切片:将长任务切分成 5ms 左右的小片(默认时间窗口),每个片执行完检查是否应该让出主线程。
- 优先级队列:不同类型的更新(Immediate、UserBlocking、Normal、Low、Idle)被放入不同优先级的队列中,调度器每次从最高优先级的队列中取任务执行。低优先级任务(如
IdlePriority)只会在没有更高优先级任务、且浏览器空闲时执行。
- 模拟
1.5 React 渲染流程
- 可以将 react 运行的主干逻辑进行概括:
- 输入: 将每一次更新(如: 新增, 删除, 修改节点之后)视为一次
更新需求(目的是要更新DOM节点). - 注册调度任务:
react-reconciler收到更新需求之后, 并不会立即构造fiber树, 而是去调度中心scheduler注册一个新任务task, 即把更新需求转换成一个task. - 执行调度任务(输出): 调度中心
scheduler通过任务调度循环来执行task(task的执行过程又回到了react-reconciler包中).fiber构造循环是task的实现环节之一, 循环完成之后会构造出最新的 fiber 树.commitRoot是task的实现环节之二, 把最新的 fiber 树最终渲染到页面上,task完成.
- 输入: 将每一次更新(如: 新增, 删除, 修改节点之后)视为一次
- react 渲染流程分为 render 和 commit 阶段:
- render 阶段(是可以打断的、是有优先级的,render有可能执行多次)执行 vdom 转 fiber 的 reconcile,commit 阶段更新 dom,执行 effect 等副作用逻辑。
- commit (同步)阶段分为 before mutation(useEffect 宏任务是异步执行的)、mutation(页面更新)、layout (useLayoutEffect) 3 个小阶段。
2. HTML CSS
2.1 Shadow DOM
Shadow DOM是 Web Components 规范的核心技术之一,用于封装一个独立的 DOM 子树,使其与主文档的 DOM 隔离。
简单说:它允许你创建组件内部的“影子 DOM 树”,其中的样式、结构、行为都不会泄露到外部,也不会被外部样式意外影响。
核心概念
- Shadow host:一个普通的 DOM 元素,作为 Shadow DOM 的附着点。
- Shadow root:附着到宿主元素上的根节点,它是 Shadow DOM 树的起点。外部文档无法直接通过
document查询到影子 DOM 内部的元素,除非通过 Shadow root 引用。 - Shadow tree:位于 Shadow root 之下的所有 DOM 节点(元素、文本、属性等)。
主要特性
样式隔离
- 影子 DOM 内部定义的 CSS 不会影响外部。
- 外部的全局 CSS 不会侵入影子内部(除了少数继承属性,如
color、font等)。 - 可通过
:host伪类选择影子宿主本身,例如:host { border: 1px solid; }。
事件封装/重定向
- 从影子内部冒泡到外部的事件会被重定向(retarget),使得
event.target看起来像是宿主元素,而不是影子内部的真实元素。这防止了外部代码直接依赖内部结构。 - 但仍然可以获取内部细节(通过
event.composedPath())。
3. JavaScript
3.1 文件上传
3.2 分片上传
3.3 File 对象和 Blob 对象的区别
File 对象和 Blob 对象都表示一个二进制数据块,它们都继承自 Blob 类。
File对象除了包含二进制数据外,还包含了文件的元数据(如文件名和修改日期)。Blob对象是包含File的。
3.4 console.log 是同步还是异步
console.log是同步的,与异步完全无关。
- chrome 的控制台出于对性能的考虑,对于引用类型的数据读取存在延迟。
- 控制台默认值读取第一层数据,当你点击展开时,才会重新去堆内存中读取属性值和下一层的数据。
3.5 柯里化
柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。例如,一个接收两个参数的函数可以被柯里化为只接收一个参数的函数,该函数返回一个新函数,该新函数接收第二个参数并返回结果。这样做的好处是可以将多个参数的函数转换为单个参数的函数,从而使其更易于组合和重用。
4. Typescript
4.1 类型断言的作用
类型断言(Type Assertion)主要出现在 TypeScript 等静态类型语言中,它的作用是手动指定一个值的类型,告诉编译器:“我比你更清楚这个变量是什么类型,请按这个类型来对待它”。
- 核心作用:
- 覆盖类型推断:当编译器无法推断出更具体的类型,而开发者根据上下文确知更精确的类型时,用来消除类型错误。
- 调用特定方法或属性:例如,
document.getElementById('id')返回HTMLElement | null,若你知道它一定是HTMLInputElement,就可以断言为HTMLInputElement,从而访问value等特有属性。 - 简化复杂类型表达:在处理联合类型或 any 类型时,将范围缩小到具体的子类型。
- 兼容非 TypeScript 数据源:如从 JSON.parse 得到的数据默认为 any,断言后可获得类型提示。
- 语法形式:
as语法:value as Type(推荐,与 JSX 兼容)- 尖括号语法:
<Type>value(在 .tsx 文件中不可用)
- 重要特性:
- 编译时行为:类型断言不会在运行时进行类型检查或转换,它只是一个“告诉编译器”的提示。如果断言错误,运行时仍可能出错。
- 不改变实际值:它不执行类型转换(如字符串转数字),只是静态类型层面的覆盖。
5. Vue2
5.1 Watcher 类型
在 Vue(包括 Vue 2 和 Vue 3)中,Watcher(观察者) 根据其用途主要分为三种类型:
- 渲染 Watcher(Render Watcher)
- 每个组件实例对应一个渲染 Watcher。
- 当组件首次渲染或数据变化导致重新渲染时,由它负责触发更新流程。
- 它会在内部执行组件的
render函数,并收集模板中使用到的响应式数据作为依赖。
- 计算属性 Watcher(Computed Watcher)
- 每个计算属性(
computed)会生成一个独有的 Watcher。 - 它负责计算属性的缓存机制:只有当依赖的数据发生变化时,才会重新求值;否则返回缓存值。
- 计算属性 Watcher 具有“惰性求值”的特点(懒执行)。
- 每个计算属性(
- 侦听器 Watcher(User Watcher / Watch Watcher)
- 由用户通过
watch选项或$watch方法显式创建。 - 用于监听指定的数据源变化,在回调函数中执行自定义逻辑(如异步操作、DOM 操作等)。
- 可以配置
immediate、deep、flush等选项控制行为。
- 由用户通过
6. Vue3
6.1 setup
- vue3 中 setup 组件有实例。
- setup 返回的 render 函数优先级高于 template。
6.2 Vue 3 的初始化过程
创建应用实例:
createApp()- 创建一个应用上下文(app context),用于存储全局配置(如 directives、components、mixins、provide 等)。
- 生成一个 app 实例,该实例包含
use()、mount()、unmount()、provide()等方法。 - 将传入的根组件(App 对象)保存为
_component。 - 响应式系统尚未初始化,因为响应式是在组件实例化时才真正建立的。
挂载应用:
app.mount('#app')mount是初始化的关键阶段,它会完成根组件从配置对象 → 组件实例 → 真实 DOM 的过程。
图示流程
~ createApp(App) → app.mount('#app') │ ├── 获取容器,清空内容 ├── 创建根组件 VNode ├── 创建根组件实例 ├── 初始化组件 │ ├── 初始化 props / slots │ ├── 调用 setup() │ ├── 处理 Options API (data, computed...) │ └── 创建响应式 ├── 调用 beforeMount ├── 创建渲染 effect setupRenderEffect() │ ├── 执行 render 函数 → 产生 VNode 树 │ ├── 递归创建子组件实例 → 重复初始化流程 │ └── patch VNode → 生成真实 DOM └── 调用 mounted
7. 网络协议
7.1 HTTP1.1 有哪些特点
- 长连接:默认使用 Connection:keep-alive(长连接),避免了连接建立和释放的开销。引入了 TCP 连接复用,即一个 TCP 默认不关闭,可以被多个请求复用
- 并发连接(谷歌是6个):对一个域名的请求允许分配多个长连接(缓解了长连接中的「队头阻塞」问题)
- 引入管道机制(pipelining):一个 TCP 连接,可以同时发送多个请求。可以不必等待请求响应就发起下一个请求,但是响应结果必须保持发送出去的顺序。有一个卡住就会把后面都卡住,这个是硬伤。(响应的顺序必须和请求的顺序一致,因此不常用)
- 增加了 PUT、DELETE、OPTIONS、PATCH 等新的方法
- 新增了一些缓存的字段(If-Modified-Since, If-None-Match)
- 请求头中引入了 range 字段,支持断点续传
- 允许响应数据分块(chunked),利于传输大文件
- 强制要求 Host 头,让互联网主机托管称为可能
7.2 HTTP2.0 有哪些特点
- 二进制协议:HTTP/1.1版本的头部信息是文本,数据部分可以是文本也可以是二进制。HTTP/2版本的头部和数据部分都是二进制,且统称为‘帧’
- 多路复用:废弃了 HTTP/1.1 中的管道,同一个TCP连接里面,客户端和服务器可以同时发送多个请求和多个响应,并且不用按照顺序来。由于服务器不用按顺序来处理响应,所以避免了“队头堵塞”的问题。
- 头部信息压缩:使用专用算法压缩头部,减少数据传输量,主要是通过服务端和客户端同时维护一张头部信息表,所有的头部信息在表里面都会有对应的记录,并且会有一个索引号,这样后面只需要发送索引号即可
- 服务端主动推送:允许服务器主动向客户推送数据
- 数据流:服务器以流stream的形式向客户端返回内容。
7.3 pipeline 和长连接的区别
pipeline的基础是长连接,它一定要建立在长连接之上。
7.4 TCP 和 UDP 的区别
- TCP 面向连接(如打电话要先拨号建立连接)提供可靠的服务。(TCP应用场景: 网页)
- UDP 是无连接的,即发送数据之前不需要建立连接,UDP 尽最大努力交付,即不保证可靠交付。UDP 具有较好的实时性,工作效率比 TCP 高,适用于对高速传输和实时性有较高的通信或广播通信。(UDP应用场景: 直播,游戏)
- 每一条 TCP 连接只能是一对一的,UDP 支持一对一,一对多,多对一和多对多的交互通信。
- UDP 分组首部开销小,TCP 首部开销 20 字节,UDP 的首部开销小,只有 8 个字节。
- TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流,UDP 是面向报文的一次交付一个完整的报文,报文不可分割,报文是 UDP 数据报处理的最小单位。
- UDP 适合一次性传输较小数据的网络应用,如 DNS,SNMP (专业的网管,比如服务器、路由器、交换机统一管理、排查错误等等)等。
7.5 TCP 三次握手、四次挥手
7.6 TCP 的滑动窗口
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区(内存占用)可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。客户端和服务器达成一致,我到底要发送多大的数据包过去给对方。
TCP也维持了一个滑动窗口,它解决是个端到端的问题,并且动态变化。
7.7 TCP 的拥塞控制
在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。
为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。
7.8 滑动窗口和拥塞控制的区别
- 滑动窗口解决我跟你自己本身的问题。
- 拥塞控制解决,我去你那里的路况拥堵问题。
- 发送方让自己的发送窗口取拥塞窗口和滑动窗口中较小的一个。
7.9 对称加密和非对称加密的区别
- 对称加密: 这把钥匙能加密也能解密。
- 非对称加密: 有两把钥匙,一把只加密(公钥),一把只解密(私钥)。
7.10 HTTPS 是绝对安全的吗
不是绝对安全的。有一种攻击手段叫做“中间人攻击”。
简单一点讲,这个中间人对浏览器冒充服务器,对服务器冒充浏览器。这样就可以拿到你们通信数据了,并且可以篡改。
如何防范:
- 不要轻易信任证书,不知名的小网站不要随意访问
- 浏览器给安全提示的是,那就是有风险的,要谨慎
- 不要随意连接公共wifi
7.11 什么是 XSS 攻击?如何防范
XSS指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
如何防范?
- 前端对表单提交,敏感字符进行转义,例如"<" , "/" 等
- 前端防范不一定是绝对安全的,因为可以抓包篡改
- 核心是后台进行处理,比如转义
7.12 什么是 CSRF 攻击?如何防范
Csrf又叫跨站请求伪造,一般需要借助XSS产生的漏洞。
那如何防范呢?
- 堵住XSS漏洞
- 在http header中添加token校验
- 校验请求http reffer
8. Webpack工程化
8.1 Webpack 和 Vite 区别
Webpack 和 Vite 是当下最主流的前端构建工具,它们的核心区别源于设计理念和底层机制的不同:Webpack 基于打包(Bundle)思路,Vite 则利用浏览器原生 ESM(ES Modules)特性实现按需编译。
核心区别一览表
维度 Webpack Vite 开发模式 打包所有模块 → 启动服务器 预构建依赖 → 直接启动服务器,按需编译源码 热更新(HMR) 整体打包后局部更新,全量构建依赖 利用 ESM 精准替换模块,速度极快 生产构建 功能成熟,可深度优化(Code Splitting、Tree Shaking) 基于 Rollup,默认配置更简洁但扩展性稍弱 启动速度 项目越大越慢(需要全量打包) 极快(不打包,只做依赖预构建) 配置复杂度 复杂,需学习 loader、plugin 等概念 简单,常用功能内置(TS、CSS、静态资源等) 生态扩展 极其丰富(loader/plugin 无数) 较新但增长快,兼容 Rollup 插件 适用场景 大型、复杂、需要精细控制的项目 现代浏览器为主的中小型项目、体验优先的场景 总结
Webpack 是“全能打包器”,Vite 是“基于 ESM 的按需编译器”。
Vite 在开发体验上实现了对 Webpack 的超越,但生产构建能力依然依赖 Rollup,一些极端场景可能不如 Webpack 灵活。目前趋势是:新项目优先考虑 Vite,老项目或高复杂度需求继续使用 Webpack。
8.2 Chunk 和 Bundle 的区别
在 Webpack 等前端构建工具中,Chunk 和 Bundle 是两个密切相关但不同阶段的概念:
- Chunk:指构建过程中的代码块。它是 Webpack 根据模块依赖关系、入口配置、动态导入(
import())等规则,将多个模块合并分组后得到的中间产物。一个 Chunk 包含一组逻辑上相关的模块(例如一个入口及其所有依赖),但此时还没有生成最终文件。 - Bundle:指构建最终输出的文件。每个 Chunk 经过优化(如压缩、拆分)后,会输出为一个或多个 Bundle 文件(通常是
.js、.css等)。 - 简单说:Chunk 是“逻辑块”,Bundle 是“物理文件”。
8.3 webpack-dev-server 和 http 服务器的区别
- webpack-dev-server 可以帮我们在本地自动启动一个 html js css 静态资源服务器。
- http 服务器是解析 http 请求,可以做接口,也可以做静态资源服务器。
8.4 Webpack 的热更新
Webpack 的热更新(Hot Module Replacement,HMR)能在应用运行时替换、添加或删除模块,而无需刷新整个页面,从而保留页面状态(如输入框内容、组件状态)。其核心是通过 WebSocket 监听文件变化 → 通知浏览器 → 运行时替换模块。
整体流程概览:
1. 启动阶段:webpack-dev-server 启动,注入 HMR runtime 到 bundle。
2. 文件变化:修改源码 → webpack 重新编译 → 生成新模块(hash/chunk)。
3. 通信:dev-server 通过 WebSocket 将新模块的 hash 发送给浏览器。
4. 浏览器:HMR runtime 对比旧模块,请求新模块内容。
5. 替换:执行模块置换逻辑(如 React 组件保留状态)。
6. 回退:若替换失败,则触发完整页面刷新。8.5 Webpack 构建流程
Webpack 的构建流程本质上是将项目中的各种资源(JS、CSS、图片等)按照依赖关系处理、合并、优化,最终输出浏览器可运行的静态文件。整个过程大致分为三个阶段:初始化 → 编译 → 输出。
整体流程图
~ 读取配置 + Shell 参数 → 初始化 Compiler → 执行插件 → 开始编译 ↓ 确定入口(entry) → 递归解析模块 → 使用 Loader 转换 → 构建依赖图 ↓ 合并 chunk → 优化 → 生成最终资源 → 输出到 output 路径Webpack 的构建流程可以概括为:
通过入口文件递归解析依赖 → 用 Loader 转换模块 → 组织成 Chunk → 优化合并 → 输出到文件夹。 整个过程基于 Tapable 插件架构,赋予了开发者极高的灵活性。
8.6 优化 Webpack 打包/构建速度
优化的核心思路是:先量化(诊断痛点)→ 再缓存(避免重复工作)→ 后并行(提升效率)→ 最后精简(减少负担)。今天大部分项目都强烈建议升级到 Webpack 5,其内置的持久化缓存是提升二次构建速度的关键。
第一步:量化诊断,找到性能瓶颈
在进行任何优化之前,最重要的一步是了解问题所在,避免“拍脑袋”式的优化。
- Speed Measure Plugin (SMP):可以量化构建过程中各个 Loader 和 Plugin 的耗时,帮你快速定位是编译、压缩还是打包阶段的问题。
- Webpack Bundle Analyzer:生成构建产物的交互式可视化分析图,直观地展示各个模块的大小及其依赖关系,有助于识别体积过大的依赖包。
第二步:核心优化三板斧(缓存、并行、范围)
- 充分利用缓存(重中之重):这是提升二次构建及 CI/CD 构建速度最有效的方法。
- Webpack 5 内置持久化缓存 (Persistent Cache):这是 Webpack 5 的王牌特性。它能将模块处理结果缓存到硬盘,重启后依然有效,极大提升二次构建速度。
- 开启 Loader 的缓存:对于
babel-loader等耗时的 Loader,务必开启其内部缓存。
- 并行处理 (Parallel Processing):在瓶颈阶段“多开线程”,榨干多核 CPU 的性能。
thread-loader:目前主流的解决方案。把它放在昂贵 Loader(如babel-loader)的前面,它就会将这些耗时的编译任务分配到 Node.js 的 Worker 池中并行处理。- 生产环境的并行压缩:配置
TerserPlugin开启parallel选项,让代码压缩过程也能利用多核优势。
- 缩小 Loader 处理范围:让 Loader 只做它该做的工作,避免不必要的操作。
- 明确
include/exclude:明确告诉 Loader 只处理src源码目录,并坚决排除node_modules。 - 优化模块解析 (Resolve):通过配置
resolve字段,减少 Webpack 寻找模块的时间。
- 明确
- 充分利用缓存(重中之重):这是提升二次构建及 CI/CD 构建速度最有效的方法。
第三步:高级优化策略
- 升级到 Webpack 5:新版本在缓存的确定性、长期缓存、Tree Shaking 等方面有巨大改进。
- 替换转译器(更快的 Rust 工具):将 Babel 对 JS/TS 的转译工作,交给用 Rust 编写的工具来大幅提速。
- 分离第三方库 (External & DLL)。
第四步:减少打包体积,以“减重”促“提速”
- 开启 Tree Shaking:在
package.json中配置"sideEffects": false,Webpack 的生产模式会自动移除未引用的代码。 - 基于模块的代码分割:利用
optimization.splitChunks将公共代码抽离,减少重复打包。但切记避免过度切割,否则会增加文件 I/O 开销。 - 优化图片:使用
image-webpack-loader等工具在构建时无损压缩图片,既减小体积也减少读写时间。
- 开启 Tree Shaking:在
第五步:终极替代 —— 换工具
如果以上方法都已用尽,但项目体量实在巨大,可以考虑更换底层由 Rust 等语言编写的构建工具,它们通常能带来数倍的性能提升。(Rspack、Rsbuild、Vite)
8.7 Webpack 打包 hash 码生成依据
Webpack 打包时生成的 hash 码(如 [hash]、[chunkhash]、[contenthash])用于文件名版本控制,确保文件内容变化时浏览器能获取最新版本。它们的生成逻辑和依赖内容各不相同,下面详细拆解。
三种 Hash 类型及其生成依据
占位符 生成依据 变更条件 典型用途 [hash]整个编译过程(所有模块 + 配置 + 构建环境) 任何一个模块或配置改变都会导致 hash 变化 单页应用所有资源共用同一版本 [chunkhash]当前 chunk 的内容(包含其所有模块的代码) 仅当该 chunk 内的任一模块内容改变时才会变化 按 entry 或动态导入分 chunk [contenthash]模块的实际内容(通常用于 CSS 等提取的文件) 仅当该文件自身内容变化时才会变化 单独提取的 CSS 文件 注意:
[hash]在 Webpack 5 中已被标记为废弃(推荐使用[fullhash],语义相同)。[chunkhash]在 Webpack 5 中也建议改用[contenthash]达到更细粒度的缓存。Webpack 的 hash 生成本质
将特定范围的内容(全部 / chunk / 单个资源)序列化为 Buffer,通过配置的哈希算法(默认 xxhash64)计算出摘要,并可按需截取长度,最终拼接到输出文件名中。
利用不同粒度的 hash 可以实现精确的长效缓存策略。
8.8 Babel 原理
babel 的转译过程也分为三个阶段,这三步具体是:
- 解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程;
- 转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作;
- ⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 babel-generator。
9. Vite工程化
9.1 Vite 在开发环境不进行 TS 类型检查
因为 es6 module 的核心是按需加载。
ts 的类型检查是项目全量,这样和 vite 的性能优势相违背。
所以,esbuild 不支持。
9.2 esbuild 只用在开发环境
它有一些天然的缺陷
- 不支持es5
- 对代码拆分支持有限
- 一些特殊的语法不支持
所以,开发环境关注性能,生产环境使用rollup弥补这些缺陷。
9.3 esbuild 为什么快
esbuild 之所以能在打包速度上拉开数量级的差距,是因为它在项目设计之初就将性能作为最高优先级,其优势是一套环环相扣的系统性设计。
核心原因概览
- 语言优势:使用 Go 语言编写,编译为原生机器码,启动极快,没有 JIT 的预热开销。
- 并行处理:Go 原生支持并行,esbuild 的算法被精心设计,能饱和式利用多核 CPU,让打包的不同阶段并行执行。
- 内存与 AST 优化:精心设计的内存布局和极少的 AST 遍历次数,最大化 CPU 缓存命中率,大幅减少内存占用。
- 纯手工构建:从零自研核心代码(Go 解析器、生成器等),不依赖第三方库,保证零妥协的性能。
- 设计取与舍:通过牺牲 Babel 般的生态灵活性,换取极致的性能。
增量构建与缓存
除了全量构建,esbuild 也优化了增量构建。它会在首次构建后缓存部分元数据,在
watch模式下,后续构建仅重新处理修改的文件。例如,1Password 团队迁移至 esbuild 后,热构建时间从约70秒降至5秒。权衡与定位
esbuild 不提供 Babel 那种高度的可配置 AST 操作能力。这种功能范围的取舍,是其能够实现极致性能的关键,正如作者所言,这是为了保持“全栈速度”的必要代价。这也解释了为什么很多大型项目(如 Vite)将 esbuild 用于开发阶段的依赖预构建,而生产环境构建则因其灵活性不足等原因,交由功能更全面的 Rollup 处理。
9.4 为什么生产环境单独打包 css
- css 打进 js,js 会太大
- 可以利用缓存优化,可能我只改了 js,没改样式
9.5 什么是 AST
AST(Abstract Syntax Tree,抽象语法树) 是源代码语法结构的一种树状表示。它将代码中的每一个语法单元(如变量、运算符、函数调用等)转化为树中的一个节点,剥离了空格、注释等无关细节,只保留逻辑结构,方便程序进行分析、转换或生成。
AST 是代码的骨架,它将人类可读的文本转换为机器(或程序)可以精确理解和操作的结构化数据。无论是编译、检查、压缩还是重构,几乎所有现代前端工具都建立在对 AST 的解析和操作之上。
直观理解
比如 JavaScript 表达式:
1 + 2 * 3,其 AST 大致如下(简化版):~ + / \ 1 * / \ 2 3AST 的关键特征
- 抽象性:只保留对编译或分析有用的信息,忽略语法糖、空白、分号(在某些语言中)等。
- 树形结构:每个节点代表一种语法构造(如
IfStatement、VariableDeclaration、BinaryExpression)。 - 语言无关:但针对特定语言的定义不同(JavaScript AST 规范常见的是 ESTree 标准)。
在前端开发中的常见用途
工具/场景 如何使用 AST 编译器/转译器 如 Babel 将 ES6+ 代码转为 ES5:解析 → 修改 AST → 生成新代码。 代码检查 ESLint 遍历 AST,检查是否符合规则(如未使用变量、分号风格)。 代码格式化 Prettier 重新打印 AST,生成统一风格的代码。 打包工具 Webpack 等分析 import/export依赖时,会解析模块的 AST。TypeScript 编译器 将 TS 源码解析为 AST,进行类型检查再生成 JS 代码。 代码压缩(Terser) 操作 AST 来重命名变量、删除死代码、合并语句。