SPA 的优点
- 用户体验好:页面切换无需刷新整个页面,提供了类原生的流畅体验,非常适合复杂的交互场景,例如在线编辑器
- 减少服务器压力:大部分逻辑和渲染工作由前端完成,服务器只需提供数据接口(API),减少了服务器的计算负担
- 职责明确:前端专注于 UI 和交互,后端专注于数据处理和业务逻辑等
MVC 是一种将应用分为三个核心部件的架构模式。
MVVM 是 MVC 的一种演进模式,核心是数据驱动,通过数据绑定让开发更高效。
| 特性 | MVC | MVVM |
|---|---|---|
| 核心思想 | 关注点分离,通过控制器手动协调 | 数据驱动,通过数据绑定自动同步 |
| 通信方式 | 单向或环形:View -> Controller -> Model -> View | 双向数据绑定:View 和 Model 通过 ViewModel 自动同步 |
| 职责 | Controller 承担了大量逻辑,是“指挥官” | ViewModel 只暴露 View 需要的数据和状态,更“被动” |
| 适用场景 | 传统后端渲染(如 Spring MVC)、jQuery 项目 | 现代 数据驱动 的前端框架(如 Vue, React, Angular) |
MVC 需要你手动告诉程序去更新视图(操作 DOM),而 MVVM 是自动的(修改数据就行)。MVVM 让前端开发更专注于数据和业务逻辑,而不是繁琐的 DOM 操作。
SPA(Single Page Application,单页面应用) 是一种现代的网页应用程序架构。在这种架构中,用户在浏览器中加载一个初始 HTML 页面后,后续的所有交互和数据更新都通过 JavaScript 动态完成,而无需重新加载整个页面。页面的内容通过 AJAX 或其他方式从服务器获取,并动态渲染到当前页面上。像是 React、Vue 这样的框架都是 SPA 的实现方式。
MPA(Multi-Page Application,多页面应用) 是传统的网页架构,每次用户导航到新页面时,都会向服务器请求一个新的 HTML 页面,服务器会生成完整的 HTML 并返回给浏览器。这里说的“传统”只是相对而言,事实上,也存在 Astro 这样的面向 MPA 的现代框架。
SPA 的优点
SPA 的缺点
MPA 的优点
MPA 的缺点
SPA 中的路由是实现页面切换和导航的核心机制,它通过管理 URL 和页面状态之间的映射、通过 JavaScript 动态更新页面内容来实现页面切换和导航功能。
用户的操作首先会引起 URL 的变化,但这种变化不会引起页面刷新。它可以通过以下两种方式实现:
# 符号。# 后面的内容不会被发送到服务器,因此更改 # 不会触发页面更新。浏览器提供了 hashchange 事件,可以监听 URL 中 # 的变化。这种方式兼容性较好,但对 SEO 优化不好。pushState(state, title, url)(向历史记录栈中添加一条新记录)、replaceState(state, title, url)(替换当前历史记录)、popstate 事件等。但这种方式需要服务器配置支持(例如访问 /about 服务器需要返回 SPA 入口文件(如 index.html),并且可能兼容性略差。Astro、Nuxt.js 和 Next.js 等现代前端元框架的核心目标都是 提升开发体验和应用性能,但侧重点不同:
简单来说:
SSR (Server Side Rendering) 是在服务器上生成完整 HTML 页面(通常是 JS 框架编写的 SPA),再发送到客户端的技术。
核心思想是:浏览器接收到的是 可以直接显示的 HTML 内容,而不是必须等待 JS 加载执行后才能显示的内容。
与之对应的技术是 客户端渲染(Client Side Rendering, CSR) :
优势:
劣势(代价):
window)现代框架常结合两者优点,如先 SSR 渲染首屏,再 CSR 处理交互。
现代框架允许混合使用,根据页面特性灵活选择。
应当选择:
不应选择:
主流 SSR 框架包括:
这些框架不仅提供 SSR 功能,还支持静态生成(SSG)等现代渲染模式,满足不同场景需求。
SSG (Static Site Generation) 是在 构建时(build time) 生成 HTML 页面的技术。与 SSR 不同,它不需要在每次请求时生成页面。
核心逻辑:构建时生成所有 HTML 页面 -> 部署到 CDN -> 用户请求 -> 直接从 CDN 获取 HTML
优点:
缺点:
ISR (Incremental Static Regeneration) 是 SSG 的增强版本,允许在运行时,在用户请求后,按需、增量地重新生成 特定页面,结合了 SSG 的性能和 SSR 的灵活性。
核心逻辑:
优点:
水合是指 客户端 JavaScript 为服务器渲染好的静态 HTML 页面附加交互性和状态的过程。简单说,就是让静态页面“活”起来,变成可以响应用户操作的单页应用,它是连接服务器端渲染和客户端交互的桥梁。
核心要点:
主要是为了在保留服务器端渲染(SSR) 优点的同时,获得客户端应用(SPA) 的交互体验。
SSR 的优点(水合要保留的):
SPA 的优点(水合要实现的):
水合就是连接这两者的桥梁,解决了 SSR 页面“只能看不能点”的问题。
流程可以简单分为四步:
解决方案:
<Suspense> 将应用拆分为独立的块。服务器不再等待所有数据,而是先发送 HTML 外壳,在 <Suspense> 包裹的组件在服务端准备好后,再将其 HTML 流式传输到客户端,替换掉原来的占位组件React 会在根节点记录这个点击事件,当对应的组件完成 Hydration 后,React 会“重放(Replay)”这个事件,确保用户的交互不会丢失。
在 React 18 之前,使用的是 ReactDOM.hydrate()。
在 React 18 及之后,推荐使用新的 ReactDOM.hydrateRoot() API 来开启并发特性(如渐进式水合)。
// 对于 React 18+import { hydrateRoot } from 'react-dom/client';const root = hydrateRoot(document.getElementById('root'), <App />);它告诉 React:“不要去创建新的 DOM 节点了,直接去‘激活’这个容器里已有的服务器渲染的 DOM 节点。”
<Suspense>React Server Components (RSC) 是一种 新型的组件类型,它 专门在服务器端运行,永远不会被发送到客户端,也永远不会进行 Hydration。
核心特性:
useState)、生命周期(useEffect)或浏览器 API,也不能包含交互性(如事件处理程序)。与 SSR 的关系:互补而非替代
可以把它们看作是解决不同问题的、可以协同工作的两种技术:
SSR:
RSC:
协同工作模式(以 Next.js App Router 为例):
一个现代 React 应用通常是混合的:
'use client' 指令):用于渲染需要交互性的部分(如表单、按钮、复杂状态逻辑),这些组件仍然可以通过 SSR 预渲染其初始 HTML。流程简化版:
RSC 是一种新的组件范式,而 SSR 是一种渲染技术。RSC 的架构天然包含了 SSR(用于其中的 Client Components),但它的目标和范围远大于传统的 SSR。
好处有代码补全会更加智能,接口所需参数等信息更加明确;挑战包括对老旧库的兼容可能存在问题,有的类型定义过于复杂、降低了可读性。
我的实践经验包括:核心逻辑、前后端连接的接口等保持强类型,UI 组件使用更宽松的类型定义
<script setup> 使用组合式 API 会比相同情况下的选项式 API 更高效,对代码压缩也更友好。(这是由于 <script setup> 形式书写的组件模板被编译为了一个内联函数,和 <script setup> 中的代码位于同一作用域。不像选项式 API 需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问 <script setup> 中定义的变量,无需从实例中代理。)原生 JS 的事件处理是基于浏览器的事件模型,分为捕获、目标和冒泡三个阶段。当事件触发时,浏览器会创建事件对象,并从根节点(如 window)开始捕获,到达目标节点后触发处理函数,再逐级冒泡返回。事件监听器通过 addEventListener 绑定,回调函数会被推入任务队列,由事件循环异步执行。开发者需要手动管理事件绑定和移除,否则可能造成内存泄漏,且需处理不同浏览器的兼容性问题。
React 的事件绑定则通过 合成事件(SyntheticEvent) 和 事件委托 实现。所有事件实际被委托到应用的根节点(React 17+)或 document(旧版本),而非直接绑定到具体 DOM 元素。React 在底层统一管理事件监听,通过事件映射表存储处理函数,触发时根据事件类型和目标元素分发对应的回调。合成事件封装了原生事件对象,提供跨浏览器一致的 API,并复用事件对象以减少内存开销。此外,React 自动绑定类组件方法的 this,且在组件卸载时自动清理事件,避免手动移除。
两者的差异主要体现在:
例如,React 中阻止事件冒泡需调用 e.stopPropagation(),而原生事件中需额外考虑捕获阶段的处理。此外,React 的合成事件在异步操作中需通过 e.persist() 保留数据,而原生事件无此限制。
阻止事件冒泡的核心目的是阻止事件向父级元素传播。但在不同环境下,实现方式不同:
.stopPropagation() 方法。.stopPropagation() 方法。虽然方法名相同,但 React 封装了浏览器原生事件,其事件对象是合成事件,但在 API 上保持了一致性。
在大型 Vue 项目中设计可维护的组件架构,核心是分层解耦和约定优先。比如组件按业务域划分到modules/目录下,每个模块包含组件、服务、类型定义。组件间通信尽量用props + emit明确定义依赖关系,避免滥用$root或事件总线导致追踪困难。对于跨层级数据,可以用provide/inject搭配 Symbol 键名防止命名冲突,或者用Pinia按模块拆分 store。
高阶组件(HOC) 的典型应用场景是 权限控制 。比如封装一个withAuth函数,接收目标组件,返回一个新组件:内部先检查用户权限,未通过时渲染 403 页面或加载骨架屏,通过后再渲染真实内容。这样所有需要权限的页面组件都可以用withAuth(ReportPage)统一处理鉴权逻辑,避免在每个组件里重复权限校验代码。HOC 还能用来做数据预加载,比如在路由跳转前通过asyncData接口获取数据,再将数据作为 props 注入目标组件。
另外,对于复杂的表单场景,可以用 HOC 封装表单校验逻辑。比如withValidation高阶组件统一处理表单字段校验、错误提示状态,被包裹的组件只需关注具体表单项的 UI 实现。这种模式让校验规则和 UI 实现彻底解耦,后续修改校验策略时不会影响业务组件。
虚拟 DOM (Virtual DOM)就是使用 JavaScript 对象在内存中构建一个轻量级的 DOM 表示形式,用来模拟真实 DOM 的结构和状态。当需要更新 UI 时,优先修改虚拟 DOM,而不是直接操作真实 DOM。框架(如 React)会通过高效的算法对比虚拟 DOM 的前后状态(称为 diff 算法 ),找出最小的变化集合,然后将这些变化批量应用到真实 DOM 上。这种方式可以显著减少直接操作真实 DOM 的次数,从而提升性能。
React 和 Vue 的 diff 算法都旨在高效更新 DOM,但实现方式有显著差异。
React 的 diff 算法基于虚拟 DOM,核心是层级比较:同一层级节点逐个对比,跨层级直接替换,依赖 key 匹配节点,列表对比是单向(从左到右)的。例如,列表头部插入元素时,React 可能触发全量移动操作。
Vue 的 diff 算法同样基于虚拟 DOM,但优化了列表对比策略。例如 Vue2 采用双端比较(头尾同时开始),能更快定位可复用节点,减少 DOM 操作;Vue3 则引入了更高效的静态提升、补丁标志和最长递增子序列优化等。
Vue 在编译阶段会标记静态节点(如不变的 DOM 结构),运行时直接跳过这些节点的对比,而 React 需要手动通过 React.memo 或 shouldComponentUpdate 优化。响应式机制上,Vue 通过 Proxy 自动追踪依赖,精准触发更新;React 则依赖不可变数据,需手动调用 setState 或使用 Hooks 触发更新。
总结来说,React 的 diff 更通用,适合复杂组件逻辑,但需开发者主动优化;Vue 在列表操作和静态内容场景下更高效,尤其适合高频动态列表(如聊天窗口)或含大量静态结构的页面。两者差异源于设计理念:React 强调显式控制,Vue 侧重编译时优化与自动追踪。
Vue2 的 diff 算法基于传统的双端比较策略(双指针算法),Vue3 的 diff 算法引入了更高效的静态标记和最长递增子序列优化。具体包括:
React 和 Vue 的核心区别主要体现在 设计哲学 上,这直接导致了不同的 开发体验。
视图编写方式:
JSX,主张“All in JavaScript”,将渲染逻辑和 UI 标签耦合在组件内,追求极致的编程灵活性和表现力。模板,基于经典的 HTML,通过指令(如 v-if, v-for) 增强,更符合传统 Web 开发者的直觉,实现了关注点分离。数据与状态管理:
单向数据流 和 Immutable 原则。状态是不可变的,更新必须使用 setState 或 useState 的 setter 函数生成一个新状态来替换旧状态。这样设计简化了状态变化的追踪,利于性能优化和调试。响应式 系统(Vue2: Object.defineProperty / Vue3: Proxy)。数据是“可变的”,修改属性值会自动触发视图更新。其内置的 v-model 提供了便捷的语法糖来实现双向绑定,降低了表单处理的复杂度。框架与生态:
学习曲线与灵活性:
Vue 通过约定和官方套件提供了更 结构化 和 易于上手 的体验;而 React 通过其极简的 API 和强大的生态系统提供了更高的 灵活性和自由度。
在 React 中,依赖项数组的核心问题源于其显式声明依赖的设计:如果漏掉依赖项,会导致闭包过时(如引用旧状态)、性能问题(冗余渲染)或逻辑错误(未正确响应数据变化)。React 需要开发者手动管理依赖关系,以确保副作用(如 useEffect)在正确时机执行。
而 Vue 的响应式系统通过 Proxy(Vue 3)或 Object.defineProperty(Vue 2)自动追踪依赖,无需手动填写依赖数组。当响应式数据被读取时,Vue 会自动记录依赖关系,并在数据变化时触发更新(如组件重新渲染或 watchEffect 执行)。这种隐式依赖收集机制减少了人为错误,但牺牲了对依赖关系的显式控制。
在 React 中,可以使用错误边界(Error Boundaries)来捕获和处理组件中的异常。
错误边界是一个特殊的 React 组件,它可以 捕获子组件树中的 JavaScript 错误,记录它们,并显示回退 UI,而不是让这些错误导致整个应用崩溃。
错误边界的重要性:
错误边界组件可以使用 componentDidCatch(error, errorInfo) 生命周期方法 或 getDerivedStateFromError(error) 静态方法 来实现(在类组件中);也可以使用 react-error-boundary 库来方便地创建错误边界组件(更适用于函数组件的场景中)。具体参考:使用错误边界捕获渲染错误 - React 中文文档
在 Vue 中,异常捕获主要通过两种机制实现:全局错误处理器和组件内的 errorCaptured 钩子,这为我们提供了从宏观到微观的全面错误处理能力。
全局错误处理器 (app.config.errorHandler):
这是捕获 Vue 应用内所有组件运行时错误的“兜底”方案。通过在应用初始化时定义一个全局的 errorHandler 函数,任何未经处理的、源自组件渲染或生命周期钩子的同步错误都会被这个函数捕获。这非常适合进行统一的错误上报、日志记录或向用户显示通用的错误提示。
import { createApp } from 'vue'const app = createApp(...)
app.config.errorHandler = (err, instance, info) => { // err: the error object // instance: the component instance that triggered the error // info: a Vue-specific error info string, e.g. "render function"
// Example: Report error to a tracking service ErrorService.capture(err, { component: instance.$options.name, info });}需要注意的是,errorHandler 无法捕获异步错误(如 setTimeout、Promise.catch 或网络请求回调中的异常),这些需要在相应的异步代码块中使用 try...catch 手动处理。
errorCaptured 生命周期钩子:
errorCaptured 钩子则提供了更精细的、组件级别的错误处理能力。一个父组件可以通过这个钩子捕获其所有后代组件(不包括自身)抛出的同步和异步错误。这使得我们可以实现更具针对性的“错误边界”(Error Boundary)模式,类似于 React 的 componentDidCatch。
当 errorCaptured 钩子被触发时,它可以:
false,它将阻止错误继续向上冒泡到更上层的 errorCaptured 钩子或全局 errorHandler。这赋予了我们精确控制错误传播路径的能力。<script setup>import { ref, errorCaptured } from 'vue'
const hasError = ref(false)
errorCaptured((err, instance, info) => { console.error('Caught error in child component:', err.message); hasError.value = true;
// Return false to prevent the error from propagating further return false;});</script>
<template> <div v-if="hasError"> <p>Something went wrong with one of our components. Please try again later.</p> </div> <div v-else> <slot></slot> <!-- Child components go here --> </div></template>通过组合使用这两种机制,我们可以构建一个既能全局兜底、又能局部优雅降级的健壮的错误处理系统。
Link 组件和 a 标签有什么区别?Link 组件是 React Router 提供的组件,用于在单页应用中实现页面跳转,它会通过 history 对象来实现页面跳转,不会重新加载页面,只会更新 URL 和渲染对应的组件。a 标签是 HTML 提供的标签,用于在页面中跳转到其他页面,会重新加载页面,不适用于单页应用。在单页应用中,应该使用 Link 组件来实现页面跳转,而不是使用 a 标签。
v-if 和 v-show 有什么区别?v-if:根据表达式的真假值,切换元素的显示状态,当表达式为 false 时,元素不会被渲染到 DOM 中。v-show:根据表达式的真假值,切换元素的显示状态,当表达式为 false 时,元素会被渲染到 DOM 中,只是通过 CSS 的 display 属性来控制元素的显示和隐藏。一般来说,当需要频繁切换元素的显示状态时,应该使用 v-show,因为它只是通过 CSS 来控制元素的显示和隐藏,不会频繁地添加和删除 DOM 元素,性能更好。而当元素的显示状态不经常变化时,应该使用 v-if,因为它会根据表达式的真假值来决定是否渲染元素到 DOM 中,可以减少不必要的 DOM 元素。
React Hooks 是 React 16.8 引入的特性,它允许你在不编写 class 的情况下使用 state 以及其他的 React 特性。以下是一些最常用的 Hooks:
useState: 这是最基础的 Hook,用于在函数组件中添加和管理状态(state)。它返回一个状态值和一个更新该状态的函数。调用更新函数会触发组件的重新渲染。
const [count, setCount] = useState(0);useEffect: 用于处理副作用(side effects),如数据获取、订阅、或手动操作 DOM。它接收一个函数作为参数,该函数在每次组件渲染后执行。通过提供第二个参数(依赖项数组),可以控制副作用的执行时机,实现类似于 componentDidMount、componentDidUpdate 和 componentWillUnmount 的生命周期功能。
useEffect(() => { document.title = `You clicked ${count} times`;}, [count]); // Only re-run the effect if count changesuseContext: 用于在组件树中进行跨层级的数据传递,避免了“属性逐层传递(prop drilling)”的繁琐。它接收一个由 React.createContext 创建的 context 对象,并返回该 context 的当前值。
const theme = useContext(ThemeContext);useReducer: useState 的替代方案,更适合管理包含多个子值的复杂状态逻辑。它接收一个 reducer 函数和初始状态,并返回当前状态和一个 dispatch 函数,用于触发状态更新。这种模式在 Redux 等状态管理库中非常常见。
const [state, dispatch] = useReducer(reducer, initialState);useCallback: 用于记忆化(memoize)回调函数。它返回一个记忆化的函数版本,只有当其依赖项之一发生变化时,该函数才会更新。这在将回调传递给依赖于引用相等性的优化子组件时非常有用,可以避免不必要的重新渲染。
const memoizedCallback = useCallback(() => { doSomething(a, b);}, [a, b]);useMemo: 用于记忆化计算结果。它会在每次渲染时执行一个函数并返回其结果,然后将该结果缓存起来。只有当其依赖项之一发生变化时,它才会重新计算。这对于避免在每次渲染时都进行高开销的计算非常有用。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);useRef: 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。useRef 创建的 ref 对象在组件的整个生命周期内保持不变。它主要有两个用途:
ref 属性上,就可以命令式地访问该 DOM 节点。const inputEl = useRef(null);这些 Hooks 的组合使用,构成了现代 React 函数式组件开发的基石,使得开发者能够以更简洁、更直观的方式构建功能强大且高效的 UI。
核心:Batch Update 和 Concurrent Rendering
这意味着,两次快速的 setState,只要它们发生于同一事件循环中,那它们就只会触发一次渲染。
React 的调度器(Scheduler)是 React 实现高效响应式更新的核心机制之一,主要解决传统同步渲染导致的页面卡顿问题。
它基于 Fiber 架构(TODO…),将渲染任务拆分成小单元,在浏览器的空闲时间中逐步执行,实现可中断、可暂停、可优先级调度的并发更新。
MessageChannel 等机制,在每帧的空闲时间执行任务,保证页面流畅。startTransition 等 API,让某些更新“可延迟”,提升响应性。父子组件间,React 可以使用 props 和回调函数;Vue 可以使用 v-model,本质上是 props 和 emit 的语法糖。
兄弟组件无法直接通信,可以通过状态提升(将共享状态提升到父组件,再通过 props 传递)、全局状态管理来实现(例如 React 直接使用 Context 或引入 Redux、Zustand 等,Vue 引入 Pinia 等)。
状态提升(Lifting State Up) :将一些组件的共享状态移至最近的共同父组件中(让父组件持有状态),再由父组件通过 props 将状态传递给子组件。若子组件需要修改状态,则通过父组件传递回调函数(Updater)给子组件来实现。
通过状态提升,实现了:
状态提升带来的问题:
React.memo 进行优化状态提升的适用场景:
在以下场景中,需要考虑替代方案:
替代方案包括:
TODO:
useImperativeHandle + forwardRef
计算属性(Computed)和方法(Method)的核心区别在于:计算属性会基于其响应式依赖进行缓存,而方法每次调用都会重新执行。
它们都用于在渲染完成后执行代码,具体而言:
// Vue 2this.$nextTick(() => { // DOM 更新完了})
// Vue 3import { nextTick } from 'vue'nextTick(() => { // DOM 更新完了})
// 微信小程序wx.nextTick(() => { // 页面渲染完了})| 区别点 | Vue | 小程序 |
|---|---|---|
| 用途 | DOM 更新后干活 | 页面渲染后干活 |
| API | Vue 自带 | wx 提供 |
| 场景 | 操作更新后的 DOM | 获取更新后的页面数据 |
TODO:
代码分隔,就是将代码从单个巨大的 bundle 拆分为多个小 chunks。这些小块可以 按需 或 并行 加载。
代码分割可以带来以下好处:
React 中可以借助 React.lazy() 和 <Suspense> 来实现代码分割:
const LazyComponent = React.lazy(() => import('./LazyComponent'));const App = () => ( {/* Suspense 的 fallback 属性接受一个 React 元素,在组件加载过程中显示 */} <Suspense fallback={<div>加载中...</div>}> <LazyComponent /> </Suspense> );}典型场景是基于路由的代码分割:
const HomePage = React.lazy(() => import("./routes/HomePage"));const AboutPage = React.lazy(() => import("./routes/AboutPage"));最佳实践:
CSS 动画:React Transition Group JS 方案:Framer Motion、React Spring 其他:GSAP、Animation.css
对于 简单的、非交互式 的进出场动画,比如通知或 Modal 的淡入淡出,可以首选 React Transition Group 搭配 CSS,因为它最轻量,且能满足需求。
对于大部分 通用的 UI 动画和手势交互,比如卡片 Hover、点击缩放、拖拽等,我会选择 Framer Motion。它的声明式 API 非常高效,能快速实现现代化的动画效果。
如果一个动画需要 极致的自然感和物理感,或者需要 响应用户的连续、快速的交互,比如一个可以被随意拖拽、甩动的卡片,可以使用 React Spring。它的物理模型能创造出无法被简单曲线模拟的真实动态。
@vueuse/motion 和 GSAP,这两种动画库的适用场景有什么不同?适用场景上:
@vueuse/motion 适合元素入场/出场过渡、简单交互反馈等轻量级动画。它和 Vue 的集成度很高,通过在元素上添加 v-motion 就可设置简单动画。例如,我使用该库实现的功能是随着滚动,列表中的元素缓入场景。ScrollTrigger。为了优化其性能:
transform:scale() 代替 width/height 变更,利用 GPU 加速ScrollTrigger 替代原生的 scroll 事件,因为它内部集成了节流和 RAF(RequestAnimationFrame)优化preload="auto"will-change: transform 提前告知浏览器优化策略