前端常见面试题
1. Vue 3 响应式原理:Proxy 相较于 Object.defineProperty 的优势
一句话概括
Object.defineProperty是对对象的每个属性进行拦截。Proxy是对整个对象进行代理,拦截所有操作,更强大高效。
Object.defineProperty(Vue 2)的痛点
- 深度递归遍历所有属性,初始化性能差,层级深时消耗大。
- 不能监听新增或删除属性,动态增删属性需特殊 API(如
$set、$delete)。 - 不能原生监听数组索引或长度变更,需要重写七个数组方法实现监听。
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`读取属性 ${key}:`, val);
return val;
},
set(newVal) {
console.log(`设置属性 ${key} 为:`, newVal);
val = newVal;
}
});
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
const data = { name: 'Vue2', age: 2 };
observe(data);
data.name; // 读取属性 name: Vue2
data.age = 3; // 设置属性 age 为: 3
// 新增属性 —— 无法监听
data.gender = 'male';
data.gender = 'female'; // 没有触发 set
// 数组监听 —— 不能监听索引变化
let arr = [1, 2, 3];
observe(arr);
arr[0] = 99; // 无法监听
arr.push(4); // Vue 2 内部会 hack push 方法

Proxy(Vue 3)的优势
- 代理整个对象,拦截所有操作(读、写、删、遍历等)。
- 支持动态新增/删除属性,无须特殊 API。
- 支持所有数组操作,包括索引赋值、长度修改等,无需 hack。
- 性能更优,惰性求值,初始化无需递归遍历。
import { reactive } from 'vue'
function reactiveWithoutLog(target) {
return new Proxy(target, {
get(obj, key) {
return obj[key]
},
set(obj, key, value) {
obj[key] = value
return true
},
deleteProperty(obj, key) {
delete obj[key]
return true
}
})
}
// 组合式 API 核心:用 reactive 包裹 Proxy
function useReactiveState() {
const state = reactive(reactiveWithoutLog({ name: 'Vue3', age: 3 }))
const arr = reactive(reactiveWithoutLog([1, 2, 3]))
function demo() {
// 访问属性
console.log('读取 name:', state.name)
// 修改属性
state.name = 'Vue3 Updated'
console.log('修改 name:', state.name)
// 新增属性
state.gender = 'male'
console.log('新增 gender:', state.gender)
// 删除属性
delete state.age
console.log('删除 age,当前 age:', state.age)
// 数组修改
arr[0] = 99
console.log('修改数组第0项:', arr[0])
arr.length = 1
console.log('修改数组长度:', arr.length)
}
return { state, arr, demo }
}
// 运行演示
const { demo } = useReactiveState()
demo()

总结
Vue 3 通过 Proxy 实现响应式,解决了 Vue 2 的多项缺陷:
- 响应式性能更优,启动更快。
- 支持动态属性新增和删除。
- 天然支持数组所有操作。
- 代码实现更简洁且易维护。
2. Composition API vs Options API
核心区别
- Options API(选项式 API):按功能类别(
data,methods,computed等)来组织代码。 - Composition API(组合式 API):按业务逻辑功能组织代码,相关状态和方法聚合在一起。
对照表
| 对比项 | Options API | Composition API |
|---|---|---|
| 代码组织方式 | 按 data / methods / computed 分块 |
按逻辑功能集中编写 |
| 逻辑聚合 | 同一功能的逻辑可能分散在多个选项中 | 同一功能的逻辑集中在一个函数或代码块内 |
| 逻辑复用 | 主要依赖 mixins(容易命名冲突、来源不清晰) | 使用 composables(可导入函数,来源清晰) |
| 类型支持 | this 推导不直观,TypeScript 支持弱一些 |
变量是普通变量,TS 推导自然 |
| 学习曲线 | 对初学者友好,结构固定 | 灵活但需要理解 Hooks 式思维 |
| 大型项目维护 | 代码易分散,跨功能修改麻烦 | 高内聚,可模块化拆分功能 |
//Options API 示例
<script>
export default {
data() {
return {
count: 0
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
//Composition API 示例
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
</script>
//逻辑复用示例 — 使用 Composition API 创建 useCounter 复用函数
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubleCount,
increment
}
}
//组件里使用:
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubleCount, increment } = useCounter()
</script>
这样通过 Composition API,功能相关的状态和方法可以集中管理,逻辑清晰,复用方便,代码更易维护。
适用场景
- Options API 更适合:
- 小型项目或简单组件
- 初学者快速上手
- Composition API 更适合:
- 中大型项目
- 功能逻辑复杂、跨组件复用多
- 需要更强的 TypeScript 支持
3. setup语法糖
核心作用
- 简化 Composition API 写法:省略
setup()方法和return,直接在<script setup>中声明即可。 - 自动暴露变量:顶层变量、方法、import 的组件都会自动暴露给模板。
- 组件自动注册:
import的组件可以直接在模板中使用,不用手动写components。 - 简化 Props & Emits:
defineProps/defineEmits宏直接声明,无需结构化props, emit参数。 - 更强的 TypeScript 支持:泛型 + 类型推导让 TS 编写体验更好。
对照表
| 对比项 | 普通 Composition API | <script setup> 语法糖 |
|---|---|---|
是否需要写 setup() |
需要 | 不需要 |
| 变量暴露 | 必须 return 才能在模板中用 |
自动暴露 |
| 组件注册 | 需写 components 选项 |
直接 import 使用 |
| Props & Emits 声明 | 在选项或 setup() 参数中定义 |
defineProps / defineEmits 宏 |
| TypeScript 支持 | 较为分散 | 更集中,类型推导更自然 |
//1. 传统 Composition API 写法(无 <script setup>)
<script>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
export default {
components: { ChildComp },
props: {
title: String
},
emits: ['update'],
setup(props, { emit }) {
const count = ref(0)
function increment() {
count.value++
emit('update', count.value)
}
return { count, increment }
}
}
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
<ChildComp />
</div>
</template>
//2. 使用 <script setup> 重写后
<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
const props = defineProps({
title: String
})
const emit = defineEmits(['update'])
const count = ref(0)
function increment() {
count.value++
emit('update', count.value)
}
</script>
<template>
<div>
<h2>{{ props.title }}</h2>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
<ChildComp />
</div>
</template>
4. Pinia vs Vuex
1. 数据修改方式
- Vuex: 必须通过
mutations修改 state。 - Pinia: 可直接在组件中修改 state,也可在
actions中改(this.count++)。
2. 模块化设计
- Vuex: 使用
modules配置,支持namespaced,结构较复杂。 - Pinia: 每个 store 独立定义,直接引入使用,更直观。
3. TypeScript 支持
- Vuex: 类型推导复杂,需要额外定义类型文件。
- Pinia: 从设计之初就支持 TS,类型推导自然。
4. 体积
- Vuex: 相对更大。
- Pinia: 仅约 1KB,轻量。
5. 开发者工具
- 两者都支持 Vue Devtools。
//Vuex 修改 state 的写法
// store.js
import { createStore } from 'vuex'
export const store = createStore({
state() {
return { count: 0 }
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment({ commit }) {
commit('increment')
}
}
})
// 组件中使用
this.$store.dispatch('increment')
console.log(this.$store.state.count)
//Pinia 修改 state 的写法
<script setup>
import { defineStore } from 'pinia'
// 定义 store(放到单独文件更好)
export const useCounterStore = defineStore('counter', () => {
// 状态
const count = ref(0)
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
// 组件中使用
const counter = useCounterStore()
// 调用方法
counter.increment()
console.log('count:', counter.count)
console.log('doubleCount:', counter.doubleCount)
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double Count: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
</div>
</template>
5. Vue Router:路由守卫
路由导航流程
- 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局的
beforeResolve守卫 - 导航被确认
- 调用全局的
afterEach钩子 - DOM 更新
beforeRouteEnter中传给next的回调函数被调用(此时可访问组件实例)
1. 全局守卫(beforeEach、afterEach)
使用场景: 全局登录权限控制、页面统计、修改标题、清理加载状态等。
-
//beforeEach(登录权限控制) import router from './router' router.beforeEach((to, from, next) => { const isLoggedIn = Boolean(localStorage.getItem('token')) if (to.meta.requiresAuth && !isLoggedIn) { next('/login') // 未登录跳转登录页 } else { next() } }) //afterEach(页面统计、修改标题) router.afterEach((to) => { document.title = to.meta.title || '默认标题' // 发送 PV 埋点等操作 })
2. 路由独享守卫(beforeEnter)
使用场景: 针对特定路由的权限校验(如管理员权限)。
-
//2. 路由独享守卫 const routes = [ { path: '/admin', component: Admin, beforeEnter: (to, from, next) => { const userRole = getUserRole() if (userRole === 'admin') { next() } else { next('/403') } } } ]
3. 组件内守卫
使用场景: 数据预加载、组件复用时刷新数据、阻止未保存离开。
//beforeRouteEnter(数据预加载)
export default {
beforeRouteEnter(to, from, next) {
fetchData().then(() => {
next(vm => {
vm.doSomething() // 访问组件实例
})
})
}
}
//beforeRouteUpdate(路由参数变化时刷新数据)
beforeRouteUpdate(to, from, next) {
this.loadData(to.params.id)
next()
}
//beforeRouteLeave(阻止误操作离开)
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
const answer = window.confirm('你有未保存内容,确定离开吗?')
answer ? next() : next(false)
} else {
next()
}
}
6. 组件通信
6.1 各种通信方式对比
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| props / emits | 父子组件通信 | 单向数据流,职责清晰,最常用 | 跨级或兄弟组件通信繁琐 |
| v-model | 父子组件通信(语法糖) | 简化双向绑定代码 | 本质是 prop + emit,适用场景单一 |
| provide / inject | 祖孙 / 跨级组件通信 | 解决了 props 层层传递的“钻透”问题 | 数据来源不直观,默认非响应式(需传递 ref 或 reactive) |
| Pinia / Vuex | 任何组件间通信(尤其大型应用) | 集中式管理,可预测、可调试 | 增加项目复杂度,小项目不必要 |
| EventBus(不推荐) | 任意组件通信 | 简单粗暴,快速实现 | 容易造成逻辑混乱、难调试、内存泄漏 |
6.2 props / emits(父子通信)
<!-- 父组件 -->
<template>
<Child :msg="parentMsg" @update-msg="handleUpdate" />
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMsg = ref('Hello from parent')
function handleUpdate(newMsg) {
parentMsg.value = newMsg
}
</script>
<!-- 子组件 Child.vue -->
<template>
<div>
<p>{{ msg }}</p>
<button @click="$emit('update-msg', 'Hello from child')">修改父消息</button>
</div>
</template>
<script setup>
const props = defineProps({ msg: String })
</script>
6.3 v-model(父子通信语法糖)
<!-- 父组件 -->
<template>
<Child v-model="text" />
<p>父组件显示: {{ text }}</p>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const text = ref('')
</script>
<!-- 子组件 Child.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
const props = defineProps({ modelValue: String })
</script>
6.4 provide / inject(跨级通信)
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const sharedMsg = ref('这是祖先提供的消息')
provide('msg', sharedMsg)
</script>
<!-- 孙子组件 -->
<script setup>
import { inject } from 'vue'
const msg = inject('msg')
</script>
<template>
<div>收到消息:{{ msg }}</div>
</template>
6.5 Pinia / Vuex(全局状态管理)
// Pinia store 例子 store/counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
// 组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.increment()
</script>
6.6 EventBus(不推荐)
// eventBus.js
import mitt from 'mitt'
export const eventBus = mitt()
// 发送事件
eventBus.emit('myEvent', { data: 123 })
// 监听事件
eventBus.on('myEvent', (payload) => {
console.log('收到事件:', payload)
})
7. Vite:启动为什么那么快?与 Webpack 的核心区别
7.1 Vite 启动快的原因
- Dev Server 利用原生 ES 模块 (Native ESM):
现代浏览器原生支持import,Vite 利用此特性,开发时只按需转换和提供模块文件。浏览器根据模块内import动态请求依赖,Vite 分别提供对应模块,无需预先整体打包。 - 使用 esbuild 进行预构建:
对第三方依赖(如vue、lodash),Vite 用esbuild预构建成 ESM 格式,打包成少数模块。esbuild速度远快于传统 JS 打包器,提升整体启动效率。
7.2 与 Webpack 的核心区别
| 特性 | Vite (开发模式) | Webpack (开发模式) |
|---|---|---|
| 核心原理 | 利用浏览器原生 ES Module,按需提供模块 | 启动前整体打包所有模块和依赖成 bundle |
| 启动速度 | 极快,秒级启动,无需整体打包 | 较慢,项目大时遍历依赖图打包耗时较长 |
| 热更新 (HMR) | 极快,只重新编译被修改模块,利用 ESM 热替换 | 较快,但需处理模块依赖关系,影响整个 bundle |
| 构建工具 | esbuild (预构建) + Rollup (生产打包) | Webpack 自带(Loader + Plugin 体系) |
7.3 Vite 与 Webpack 启动流程示意
项目结构:
src/
main.js
utils.js
componentA.jsVite(按需模块加载)
- 浏览器请求
main.js,Vite 按需编译并返回main.js。- 浏览器解析
main.js中的import,再请求utils.js和componentA.js。- Vite 按需编译并返回
utils.js和componentA.js。- 模块逐个加载,启动速度快,无需先整体打包。
Webpack(启动时整体打包)
Webpack 在启动时:
- 遍历
main.js的依赖图(utils.js、componentA.js)。- 将所有模块打包成一个大的
bundle.js。- 浏览器加载整个
bundle.js。- 启动时耗时较长,尤其项目依赖多时。
8. 图片懒加载和路由懒加载
8.1 图片懒加载(Image Lazy Loading)
-
作用:
延迟加载当前视口外的图片,减少页面初次加载资源,提升渲染速度和用户体验。 -
方式一:原生
loading="lazy"<img src="image.jpg" alt="描述性替代文本" loading="lazy" width="200" height="200">浏览器会自动延迟加载图片,简单高效,但兼容性有限。
-
方式二:使用 Intersection Observer API
利用浏览器提供的接口检测图片是否进入视口,进入时才加载真实图片。
<script setup>
import { ref, onMounted } from 'vue'
const imgSrc = ref('')
const placeholder = 'placeholder.jpg'
let imgRef = null
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
imgSrc.value = 'real-image.jpg'
observer.unobserve(entry.target)
}
})
})
if (imgRef) observer.observe(imgRef)
})
</script>
<template>
<img :src="imgSrc || placeholder" ref="imgRef" alt="懒加载图片" />
</template>
这段代码的核心思想是:先显示一张轻量的占位符图片,而不是立即加载所有图片。只有当图片即将进入用户的视口时,才动态地加载真实的图片。这样做可以显著减少页面初次加载时的资源消耗,提升用户体验。
8.2 路由懒加载(Route Lazy Loading)
- 作用:
路由组件只有在访问对应路由时才加载,减小首屏打包体积,加快页面初始加载速度。 - Vue Router 实现示例:
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue') // 懒加载
},
{
path: '/about',
component: () => import('@/views/About.vue') // 懒加载
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
9. 资源压缩和缓存
9.1 资源压缩
- JS/CSS 压缩
使用工具(如 Vite 默认集成的 Terser)去除空格、注释,缩短变量名,减少文件体积,加快加载。 - 图片压缩
利用imagemin、TinyPNG等压缩图片体积,同时保证视觉质量。
推荐使用现代图片格式 WebP,压缩率更高,且兼容性较好。 - 传输压缩(Gzip / Brotli)
服务器(如 Nginx)开启压缩功能,传输文本资源时自动压缩,浏览器端自动解压,节省传输带宽。
http {
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 256;
}
9.2 缓存机制:强缓存与协商缓存
| 缓存类型 | 相关 Headers | 工作流程 | 优点 | 缺点 |
|---|---|---|---|---|
| 强缓存 | Expires(HTTP/1.0),Cache-Control: max-age(HTTP/1.1) |
浏览器直接使用缓存,过期前不请求服务器 | 速度最快,无请求 | 资源变更需等缓存过期 |
| 协商缓存 | Last-Modified / If-Modified-Since,ETag / If-None-Match |
浏览器带条件请求,服务器判断资源是否变更,未变则返回 304 | 保证资源最新,节省流量 | 仍需一次请求开销 |
9.3 强缓存示例说明
服务器响应:
Cache-Control: max-age=3600
- 说明服务器告知浏览器:这个资源可以缓存,且缓存时间为 3600 秒(1小时)。
- 浏览器拿到这个响应后,会将资源和这个缓存时间一起存储。
- 在这1小时内,浏览器再次请求该资源时,不会向服务器发送请求,直接从本地缓存读取,速度最快。
9.4 协商缓存示例说明
首次服务器响应:
Last-Modified: Tue, 01 Aug 2025 10:00:00 GMT
ETag: "12345abcde"
Last-Modified:表示该资源最后修改时间,服务器告诉浏览器资源创建或修改的时间。ETag:是资源的唯一标识符(类似指纹),对比文件内容是否变动更准确。
浏览器再次请求时带的请求头:
If-Modified-Since: Tue, 01 Aug 2025 10:00:00 GMT
If-None-Match: "12345abcde"
If-Modified-Since会让服务器比对资源的最后修改时间。If-None-Match会让服务器比对资源的 ETag 标识。- 两者任何一个判断资源未变,服务器就返回
304 Not Modified,浏览器继续用缓存。
服务器返回 304 响应:
HTTP/1.1 304 Not Modified
- 资源未变,响应无正文,浏览器直接使用缓存内容。
如果资源变了,服务器返回新的内容和新的头:
HTTP/1.1 200 OK
Last-Modified: Wed, 02 Aug 2025 08:00:00 GMT
ETag: "67890fghij"
[新的资源内容]
- 服务器告知浏览器资源已经变更,浏览器更新缓存内容。
9.5 总结
- 强缓存适合资源短期不变,直接读取缓存无请求,速度最快。
- 协商缓存确保资源最新,但每次至少发一次请求,节省带宽。
10. Sass/Less:解决了原生 CSS 的哪些痛点?
痛点1:无变量
- 问题:原生 CSS 不能定义变量,颜色、尺寸等多处写重复,修改麻烦。
- 解决:Sass/Less 支持变量,统一管理,修改方便。
// Sass 变量
$primary-color: #3498db;
.button {
background-color: $primary-color;
}
痛点2:代码重复
- 问题:相同样式多处写,维护困难。
- 解决:支持
Mixin混入,封装复用。
@mixin center {
display: flex;
justify-content: center;
align-items: center;
}
.box {
@include center;
height: 100px;
}
痛点3:结构不清晰
- 问题:CSS 选择器写法平铺,层级关系不明显,难维护。
- 解决:支持嵌套写法,层级结构更清晰。
.nav {
ul {
margin: 0;
padding: 0;
li {
list-style: none;
a {
color: blue;
}
}
}
}
痛点4:缺乏模块化
- 问题:原生 CSS 文件庞大,难拆分和管理。
- 解决:支持
@import/@use拆分文件,方便维护。
| 特性 | @use |
@import |
|---|---|---|
| 命名空间 | 有(默认是文件名) | 没有(全局暴露) |
| 访问方式 | namespace.variable |
variable |
| 私有成员 | 支持(下划线开头) | 不支持 |
| 复用 | 只编译一次 | 可能导致重复代码 |
| 推荐状态 | Sass 新版本推荐使用 | 已过时,将逐步被废弃 |
// _variables.scss
$font-stack: Helvetica, sans-serif;
// main.scss
@use 'variables';
body {
font-family: variables.$font-stack;
}
痛点5:逻辑运算能力弱
- 问题:原生 CSS 无法做条件、循环等逻辑处理。
- 解决:Sass/Less 支持条件判断、循环等逻辑。
@for $i from 1 through 3 {
.col-#{$i} {
width: 100% / 3 * $i;
}
}
11. mixin, extend,placeholder 的区别是什么?
| 特性 | @mixin(混入) | @extend(继承) | %placeholder(占位符选择器) |
|---|---|---|---|
| 原理 | 复制粘贴样式代码 | 合并选择器,多个类共享同一套样式 | 类似 @extend,但不会单独生成样式,避免冗余 |
| 生成的 CSS | 每个调用都会复制一份代码,可能冗余 | 生成合并选择器,避免重复样式 | 只有被 @extend 调用时才生成对应 CSS |
| 适用场景 | 需要动态传参,生成多种变体样式 | 简单共享样式,减少重复,无法传参 | 写基础样式模板,只有被继承时才生效,避免无用代码 |
| 缺点 | 可能生成冗余 CSS,增大文件体积 | 选择器变复杂,增加 CSS 权重,可能导致样式冲突 | 只能配合 @extend 使用,不能单独调用 |
1. @mixin(混入)
@mixin btn($color) {
padding: 10px 20px;
background-color: $color;
border-radius: 4px;
color: white;
}
.btn-primary {
@include btn(blue);
}
.btn-danger {
@include btn(red);
}
结果:
.btn-primary {
padding: 10px 20px;
background-color: blue;
border-radius: 4px;
color: white;
}
.btn-danger {
padding: 10px 20px;
background-color: red;
border-radius: 4px;
color: white;
}
会复制相同样式,文件体积增大。
2. @extend(继承)
.btn-base {
padding: 10px 20px;
border-radius: 4px;
color: white;
}
.btn-primary {
@extend .btn-base;
background-color: blue;
}
.btn-danger {
@extend .btn-base;
background-color: red;
}
结果:
.btn-base, .btn-primary, .btn-danger {
padding: 10px 20px;
border-radius: 4px;
color: white;
}
.btn-primary {
background-color: blue;
}
.btn-danger {
background-color: red;
}
样式合并,减少重复。
3. %placeholder(占位符)
%btn-base {
padding: 10px 20px;
border-radius: 4px;
color: white;
}
.btn-primary {
@extend %btn-base;
background-color: blue;
}
.btn-danger {
@extend %btn-base;
background-color: red;
}
结果:
.btn-primary, .btn-danger {
padding: 10px 20px;
border-radius: 4px;
color: white;
}
.btn-primary {
background-color: blue;
}
.btn-danger {
background-color: red;
}
区别是: %btn-base 本身不会单独生成 .btn-base 样式,避免无用代码。
总结
- 用需要动态参数和多变样式时用
@mixin。 - 需要多个选择器共享基础样式,且不传参数时用
@extend。 - 想写基础样式模板,但不想生成冗余类时用
%placeholder。
12. ECharts:复杂图表实现案例
场景说明
实现一个展示服务器实时 CPU 占用率的动态折线图,数据来源模拟后端实时推送或定时轮询。
核心流程
- 初始化图表
使用echarts.init初始化图表实例,并设置好初始的option,其中数据数组可先设为空或初始值。 - 动态数据更新
实时从后端(WebSocket、轮询等)获取数据后,更新图表中的数据。通过调用setOption只传递变化部分,实现高效更新。 - 性能优化技巧
- 关闭动画 (
animation: false) 减少渲染开销。 - 使用
appendData进行增量渲染(大数据场景)。 - 使用
dataZoom组件支持缩放,避免一次渲染过多数据点。 - 数据抽样减少点数。
- 关闭动画 (
<script setup>
import * as echarts from 'echarts'
import { ref, onMounted, onBeforeUnmount } from 'vue'
// 图表容器的 ref
const chartRef = ref(null)
let chartInstance = null
// 模拟初始数据,50个数据点
const dataCount = 50
const data = Array(dataCount).fill(0).map(() => Math.random() * 50 + 10)
// 初始配置
function getOption() {
return {
title: {
text: '服务器 CPU 占用率(动态)',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: Array(dataCount).fill('').map((_, i) => i.toString()),
boundaryGap: false
},
yAxis: {
type: 'value',
min: 0,
max: 100
},
series: [
{
name: 'CPU %',
type: 'line',
data,
smooth: true,
animation: false // 关闭动画,提升性能
}
],
dataZoom: [
{
type: 'slider', // 支持拖动缩放
start: 0,
end: 100
}
]
}
}
// 模拟动态数据更新
function updateData() {
// 移除最旧的数据,添加最新的数据
data.shift()
data.push(Math.random() * 50 + 10)
// 只更新 series 的 data 部分,提升性能
chartInstance.setOption({
series: [{ data }]
})
}
onMounted(() => {
// 初始化图表实例
chartInstance = echarts.init(chartRef.value)
chartInstance.setOption(getOption())
// 模拟每秒更新数据
const timer = setInterval(updateData, 1000)
onBeforeUnmount(() => {
clearInterval(timer) // 清除定时器
chartInstance.dispose() // 销毁图表实例,避免内存泄漏
})
})
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
</template>
<style scoped>
/* 容器大小决定图表大小 */
</style>
13. React 常用 Hooks 详解及示例
1. useState
-
作用:在函数组件中声明状态变量,返回当前状态和更新函数。
-
用法示例:
import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); // 初始值为0 return ( <div> <p>当前计数:{count}</p> <button onClick={() => setCount(count + 1)}>增加</button> </div> ); }
2. useEffect
- 作用:处理副作用,比如数据请求、事件监听、订阅等。
- 依赖项数组说明:
| 依赖项 | 执行时机 |
|---|---|
| 不传(无第二参) | 每次组件渲染后都会执行回调 |
[](空数组) |
只在组件首次渲染后执行,相当于 componentDidMount |
[a, b] |
首次渲染后执行,且依赖 a 或 b 变化时重新执行 |
-
示例:组件首次加载时请求数据
import React, { useState, useEffect } from 'react'; function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(data => setUsers(data)); }, []); // 只执行一次 return ( <ul> {users.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); }
3. useContext
-
作用:获取 Context 的值,避免多层组件传递 props。
-
示例:
import React, { createContext, useContext } from 'react'; const ThemeContext = createContext('light'); function Toolbar() { const theme = useContext(ThemeContext); return <div>当前主题:{theme}</div>; } function App() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); }
4. useMemo
-
作用:缓存计算结果,只有依赖变化时重新计算,提升性能。
-
示例:
import React, { useMemo, useState } from 'react'; function Fibonacci({ n }) { const fib = useMemo(() => { function calcFib(num) { if (num <= 1) return num; return calcFib(num - 1) + calcFib(num - 2); } return calcFib(n); }, [n]); return <div>斐波那契数列第 {n} 项是 {fib}</div>; }
5. useCallback
-
作用:缓存函数实例,避免子组件无谓重渲染。
-
示例:
import React, { useState, useCallback } from 'react'; function Child({ onClick }) { console.log('Child渲染'); return <button onClick={onClick}>点击</button>; } function Parent() { const [count, setCount] = useState(0); // 缓存函数实例,只在 count 变化时更新 const handleClick = useCallback(() => { setCount(c => c + 1); }, []); return ( <div> <p>计数:{count}</p> <Child onClick={handleClick} /> </div> ); }
14. 函数组件 vs 类组件
1. 主要差异
| 特性 | 函数组件 | 类组件 |
|---|---|---|
| 语法 | 纯函数,写法简洁 | ES6 Class,需要继承 React.Component |
| State 管理 | 使用 Hook,如 useState |
使用 this.state 和 this.setState |
| 生命周期 | 使用 Hook,如 useEffect 模拟生命周期 |
有明确生命周期方法,如 componentDidMount, componentDidUpdate |
this 指向 |
无需关心 this |
需要显式绑定 this,常见错误点 |
2. 函数组件示例
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('组件挂载或 count 更新了,当前 count:', count);
}, [count]);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
3. 类组件示例
import React from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
componentDidMount() {
console.log('组件挂载完成');
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log('count 更新了,当前 count:', this.state.count);
}
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>计数: {this.state.count}</p>
<button onClick={this.handleClick}>增加</button>
</div>
);
}
}
4. 函数组件优势总结
- 代码更简洁、逻辑更集中: 组件的状态和副作用逻辑都能写在一起,避免分散在多个生命周期方法中。
- 更方便复用状态逻辑: 通过自定义 Hook(
useXxx)实现状态逻辑的封装和复用。 - 避免
this指向错误: 函数组件中不存在this,避免了类组件中常见的绑定问题。 - 符合 React 未来发展方向: React 官方推荐函数组件 + Hook,未来新特性更多面向函数组件。

浙公网安备 33010602011771号