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] = valuereturn 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.ageconsole.log('删除 age,当前 age:', state.age)// 数组修改arr[0] = 99console.log('修改数组第0项:', arr[0])arr.length = 1console.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 = nullonMounted(() => {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); // 初始值为0return (<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,未来新特性更多面向函数组件。