第35天:Element Plus 响应式系统与数据绑定
学习目标
- 深入理解 Vue 3 响应式系统在 Element Plus 中的应用
- 掌握双向数据绑定的实现原理
- 学习响应式数据的优化技巧
- 实践复杂数据绑定场景
学习内容
1. Vue 3 响应式系统基础
1.1 响应式原理回顾
typescript
// Vue 3 响应式系统核心概念
import {
ref,
reactive,
computed,
watch,
watchEffect,
readonly,
shallowRef,
shallowReactive,
markRaw,
toRef,
toRefs
} from 'vue'
// 基础响应式数据
const count = ref(0)
const state = reactive({
name: 'Element Plus',
version: '2.0.0',
features: ['TypeScript', 'Vue 3', 'Composition API']
})
// 计算属性
const doubleCount = computed(() => count.value * 2)
const displayName = computed(() => `${state.name} v${state.version}`)
// 监听器
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
watchEffect(() => {
console.log(`Current count: ${count.value}`)
})
1.2 响应式数据类型选择
typescript
// 不同响应式 API 的使用场景
class ResponsiveDataManager {
// 基础值使用 ref
private loading = ref(false)
private error = ref<string | null>(null)
// 对象使用 reactive
private formData = reactive({
username: '',
email: '',
profile: {
avatar: '',
bio: ''
}
})
// 大型对象使用 shallowReactive 优化性能
private largeDataset = shallowReactive({
items: [] as any[],
metadata: {}
})
// 不需要响应式的数据使用 markRaw
private staticConfig = markRaw({
apiUrl: 'https://api.example.com',
timeout: 5000
})
// 只读数据
private readonlyState = readonly(state)
// 获取响应式数据的引用
getFormRefs() {
return toRefs(this.formData)
}
// 获取单个属性的引用
getUsernameRef() {
return toRef(this.formData, 'username')
}
}
2. Element Plus 中的数据绑定
2.1 v-model 实现原理
vue
<!-- Element Plus Input 组件的 v-model 实现 -->
<template>
<div class="el-input">
<input
:value="modelValue"
@input="handleInput"
@change="handleChange"
@blur="handleBlur"
@focus="handleFocus"
/>
</div>
</template>
<script setup lang="ts">
interface InputProps {
modelValue?: string | number
placeholder?: string
disabled?: boolean
readonly?: boolean
}
interface InputEmits {
'update:modelValue': [value: string | number]
change: [value: string | number]
input: [value: string | number]
blur: [event: FocusEvent]
focus: [event: FocusEvent]
}
const props = withDefaults(defineProps<InputProps>(), {
modelValue: '',
placeholder: '',
disabled: false,
readonly: false
})
const emit = defineEmits<InputEmits>()
// 处理输入事件
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
const value = target.value
// 触发 v-model 更新
emit('update:modelValue', value)
emit('input', value)
}
// 处理变化事件
const handleChange = (event: Event) => {
const target = event.target as HTMLInputElement
emit('change', target.value)
}
// 处理焦点事件
const handleBlur = (event: FocusEvent) => {
emit('blur', event)
}
const handleFocus = (event: FocusEvent) => {
emit('focus', event)
}
</script>
2.2 自定义 v-model 修饰符
vue
<template>
<div class="custom-input">
<input
:value="displayValue"
@input="handleInput"
@blur="handleBlur"
/>
</div>
</template>
<script setup lang="ts">
interface CustomInputProps {
modelValue: string
modelModifiers?: {
capitalize?: boolean
trim?: boolean
number?: boolean
}
}
const props = withDefaults(defineProps<CustomInputProps>(), {
modelValue: '',
modelModifiers: () => ({})
})
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
// 显示值处理
const displayValue = computed(() => {
let value = props.modelValue
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
return value
})
// 输入处理
const handleInput = (event: Event) => {
let value = (event.target as HTMLInputElement).value
// 应用修饰符
if (props.modelModifiers.trim) {
value = value.trim()
}
if (props.modelModifiers.number) {
const numValue = parseFloat(value)
if (!isNaN(numValue)) {
value = numValue.toString()
}
}
emit('update:modelValue', value)
}
// 失焦时应用修饰符
const handleBlur = () => {
let value = props.modelValue
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
if (value !== props.modelValue) {
emit('update:modelValue', value)
}
}
</script>
3. 复杂数据绑定场景
3.1 嵌套对象数据绑定
vue
<template>
<div class="nested-form">
<el-form :model="formData" ref="formRef">
<!-- 基础信息 -->
<el-form-item label="姓名" prop="basic.name">
<el-input v-model="formData.basic.name" />
</el-form-item>
<el-form-item label="年龄" prop="basic.age">
<el-input-number v-model="formData.basic.age" />
</el-form-item>
<!-- 地址信息 -->
<el-form-item label="省份" prop="address.province">
<el-select v-model="formData.address.province" @change="handleProvinceChange">
<el-option
v-for="province in provinces"
:key="province.code"
:label="province.name"
:value="province.code"
/>
</el-select>
</el-form-item>
<el-form-item label="城市" prop="address.city">
<el-select v-model="formData.address.city" :disabled="!formData.address.province">
<el-option
v-for="city in availableCities"
:key="city.code"
:label="city.name"
:value="city.code"
/>
</el-select>
</el-form-item>
<!-- 动态字段 -->
<div v-for="(field, index) in formData.dynamicFields" :key="index">
<el-form-item :label="field.label" :prop="`dynamicFields.${index}.value`">
<el-input v-model="field.value" />
<el-button @click="removeDynamicField(index)" type="danger" size="small">
删除
</el-button>
</el-form-item>
</div>
<el-button @click="addDynamicField">添加字段</el-button>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, computed, watch } from 'vue'
// 表单数据结构
interface FormData {
basic: {
name: string
age: number | null
}
address: {
province: string
city: string
}
dynamicFields: Array<{
label: string
value: string
}>
}
// 响应式表单数据
const formData = reactive<FormData>({
basic: {
name: '',
age: null
},
address: {
province: '',
city: ''
},
dynamicFields: []
})
// 省份数据
const provinces = ref([
{ code: 'beijing', name: '北京' },
{ code: 'shanghai', name: '上海' },
{ code: 'guangdong', name: '广东' }
])
// 城市数据
const cities = ref({
beijing: [{ code: 'beijing', name: '北京市' }],
shanghai: [{ code: 'shanghai', name: '上海市' }],
guangdong: [
{ code: 'guangzhou', name: '广州市' },
{ code: 'shenzhen', name: '深圳市' }
]
})
// 可用城市计算属性
const availableCities = computed(() => {
return formData.address.province ? cities.value[formData.address.province] || [] : []
})
// 监听省份变化,重置城市
watch(
() => formData.address.province,
(newProvince) => {
formData.address.city = ''
}
)
// 处理省份变化
const handleProvinceChange = (value: string) => {
console.log('Province changed to:', value)
}
// 动态字段操作
const addDynamicField = () => {
formData.dynamicFields.push({
label: `字段${formData.dynamicFields.length + 1}`,
value: ''
})
}
const removeDynamicField = (index: number) => {
formData.dynamicFields.splice(index, 1)
}
</script>
3.2 数组数据绑定
vue
<template>
<div class="array-binding">
<!-- 表格数据绑定 -->
<el-table :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="姓名">
<template #default="{ row, $index }">
<el-input v-model="row.name" @change="handleCellChange($index, 'name', $event)" />
</template>
</el-table-column>
<el-table-column prop="age" label="年龄">
<template #default="{ row, $index }">
<el-input-number
v-model="row.age"
@change="handleCellChange($index, 'age', $event)"
/>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ $index }">
<el-button @click="removeRow($index)" type="danger" size="small">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-actions">
<el-button @click="addRow">添加行</el-button>
<el-button @click="batchUpdate" :disabled="selectedRows.length === 0">
批量更新
</el-button>
</div>
<!-- 列表数据绑定 -->
<el-card title="动态列表">
<draggable
v-model="listData"
@change="handleListChange"
item-key="id"
>
<template #item="{ element, index }">
<div class="list-item">
<el-input v-model="element.title" placeholder="标题" />
<el-input v-model="element.description" placeholder="描述" />
<el-button @click="removeListItem(index)" type="danger" size="small">
删除
</el-button>
</div>
</template>
</draggable>
<el-button @click="addListItem">添加项目</el-button>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import draggable from 'vuedraggable'
// 表格数据
const tableData = reactive([
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 }
])
// 选中的行
const selectedRows = ref([])
// 列表数据
const listData = ref([
{ id: 1, title: '项目1', description: '描述1' },
{ id: 2, title: '项目2', description: '描述2' }
])
// 表格操作
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
const handleCellChange = (index: number, field: string, value: any) => {
console.log(`Row ${index}, field ${field} changed to:`, value)
// 可以在这里添加验证逻辑
}
const addRow = () => {
const newId = Math.max(...tableData.map(item => item.id)) + 1
tableData.push({
id: newId,
name: '',
age: 0
})
}
const removeRow = (index: number) => {
tableData.splice(index, 1)
}
const batchUpdate = () => {
selectedRows.value.forEach(row => {
row.age += 1
})
}
// 列表操作
const handleListChange = (event: any) => {
console.log('List changed:', event)
}
const addListItem = () => {
const newId = Math.max(...listData.value.map(item => item.id)) + 1
listData.value.push({
id: newId,
title: '',
description: ''
})
}
const removeListItem = (index: number) => {
listData.value.splice(index, 1)
}
</script>
4. 响应式性能优化
4.1 避免不必要的响应式
typescript
// 性能优化技巧
import { shallowRef, shallowReactive, markRaw, readonly } from 'vue'
class PerformanceOptimizedComponent {
// 大型数据集使用 shallowRef
private largeDataset = shallowRef<any[]>([])
// 嵌套对象使用 shallowReactive
private complexObject = shallowReactive({
metadata: {},
items: [],
config: {}
})
// 静态配置使用 markRaw
private staticConfig = markRaw({
apiEndpoints: {
users: '/api/users',
posts: '/api/posts'
},
constants: {
pageSize: 20,
timeout: 5000
}
})
// 只读数据
private readonlyData = readonly({
version: '1.0.0',
buildTime: Date.now()
})
// 更新大型数据集
updateLargeDataset(newData: any[]) {
// 直接替换引用,避免深度响应式
this.largeDataset.value = newData
}
// 更新复杂对象的特定部分
updateComplexObjectItem(key: string, value: any) {
// 只更新需要的部分
this.complexObject[key] = value
}
}
4.2 计算属性优化
typescript
// 计算属性优化策略
import { computed, ref, shallowRef } from 'vue'
class ComputedOptimization {
private rawData = shallowRef<any[]>([])
private filters = ref({
search: '',
category: '',
status: ''
})
// 分层计算,避免重复计算
private searchFiltered = computed(() => {
if (!this.filters.value.search) return this.rawData.value
const search = this.filters.value.search.toLowerCase()
return this.rawData.value.filter(item =>
item.name.toLowerCase().includes(search)
)
})
private categoryFiltered = computed(() => {
if (!this.filters.value.category) return this.searchFiltered.value
return this.searchFiltered.value.filter(item =>
item.category === this.filters.value.category
)
})
private finalFiltered = computed(() => {
if (!this.filters.value.status) return this.categoryFiltered.value
return this.categoryFiltered.value.filter(item =>
item.status === this.filters.value.status
)
})
// 缓存复杂计算结果
private expensiveComputation = computed(() => {
console.log('Expensive computation running...')
return this.finalFiltered.value.map(item => ({
...item,
computedValue: this.performExpensiveCalculation(item)
}))
})
private performExpensiveCalculation(item: any) {
// 模拟复杂计算
let result = 0
for (let i = 0; i < 1000; i++) {
result += Math.random() * item.value
}
return result
}
// 公共接口
get filteredData() {
return this.expensiveComputation.value
}
updateFilters(newFilters: Partial<typeof this.filters.value>) {
Object.assign(this.filters.value, newFilters)
}
}
5. 响应式调试
5.1 响应式调试工具
typescript
// 响应式调试工具
import { watchEffect, watch } from 'vue'
class ReactivityDebugger {
private debugMode = ref(false)
// 监听所有响应式变化
trackReactivity(target: any, name: string) {
if (!this.debugMode.value) return
watchEffect(() => {
console.log(`[Reactivity Debug] ${name} changed:`, target)
})
}
// 监听特定属性变化
trackProperty(target: any, property: string, name: string) {
if (!this.debugMode.value) return
watch(
() => target[property],
(newValue, oldValue) => {
console.log(`[Property Debug] ${name}.${property}:`, {
old: oldValue,
new: newValue
})
},
{ deep: true }
)
}
// 性能监控
measureReactivity(fn: () => void, name: string) {
if (!this.debugMode.value) {
fn()
return
}
const start = performance.now()
fn()
const end = performance.now()
console.log(`[Performance Debug] ${name} took ${end - start}ms`)
}
enableDebug() {
this.debugMode.value = true
}
disableDebug() {
this.debugMode.value = false
}
}
// 使用示例
const debugger = new ReactivityDebugger()
// 在开发环境启用调试
if (process.env.NODE_ENV === 'development') {
debugger.enableDebug()
}
6. 实践练习
练习1:复杂表单数据绑定
vue
<template>
<div class="complex-form-binding">
<el-form :model="formData" :rules="rules" ref="formRef">
<!-- 用户基础信息 -->
<el-card title="基础信息">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用户名" prop="user.username">
<el-input v-model="formData.user.username" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="user.email">
<el-input v-model="formData.user.email" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 权限配置 -->
<el-card title="权限配置">
<el-form-item label="角色" prop="permissions.role">
<el-select v-model="formData.permissions.role" @change="handleRoleChange">
<el-option label="管理员" value="admin" />
<el-option label="用户" value="user" />
<el-option label="访客" value="guest" />
</el-select>
</el-form-item>
<el-form-item label="权限" prop="permissions.abilities">
<el-checkbox-group v-model="formData.permissions.abilities">
<el-checkbox
v-for="ability in availableAbilities"
:key="ability.value"
:label="ability.value"
:disabled="ability.disabled"
>
{{ ability.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-card>
<!-- 动态配置项 -->
<el-card title="配置项">
<div v-for="(config, index) in formData.configs" :key="index">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item :prop="`configs.${index}.key`">
<el-input v-model="config.key" placeholder="配置键" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :prop="`configs.${index}.value`">
<el-input v-model="config.value" placeholder="配置值" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-button @click="removeConfig(index)" type="danger">
删除
</el-button>
</el-col>
</el-row>
</div>
<el-button @click="addConfig">添加配置</el-button>
</el-card>
</el-form>
<div class="form-actions">
<el-button @click="resetForm">重置</el-button>
<el-button type="primary" @click="submitForm">提交</el-button>
</div>
<!-- 数据预览 -->
<el-card title="数据预览">
<pre>{{ JSON.stringify(formData, null, 2) }}</pre>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, computed, watch } from 'vue'
// 表单数据类型
interface FormData {
user: {
username: string
email: string
}
permissions: {
role: string
abilities: string[]
}
configs: Array<{
key: string
value: string
}>
}
// 响应式表单数据
const formData = reactive<FormData>({
user: {
username: '',
email: ''
},
permissions: {
role: '',
abilities: []
},
configs: []
})
// 可用权限计算属性
const availableAbilities = computed(() => {
const baseAbilities = [
{ label: '读取', value: 'read', disabled: false },
{ label: '写入', value: 'write', disabled: false },
{ label: '删除', value: 'delete', disabled: false },
{ label: '管理', value: 'admin', disabled: false }
]
// 根据角色限制权限
if (formData.permissions.role === 'guest') {
return baseAbilities.map(ability => ({
...ability,
disabled: ability.value !== 'read'
}))
}
if (formData.permissions.role === 'user') {
return baseAbilities.map(ability => ({
...ability,
disabled: ability.value === 'admin'
}))
}
return baseAbilities
})
// 监听角色变化,自动调整权限
watch(
() => formData.permissions.role,
(newRole) => {
if (newRole === 'guest') {
formData.permissions.abilities = formData.permissions.abilities.filter(
ability => ability === 'read'
)
} else if (newRole === 'user') {
formData.permissions.abilities = formData.permissions.abilities.filter(
ability => ability !== 'admin'
)
}
}
)
// 表单验证规则
const rules = {
'user.username': [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
'user.email': [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
'permissions.role': [
{ required: true, message: '请选择角色', trigger: 'change' }
]
}
// 事件处理
const handleRoleChange = (value: string) => {
console.log('Role changed to:', value)
}
const addConfig = () => {
formData.configs.push({
key: '',
value: ''
})
}
const removeConfig = (index: number) => {
formData.configs.splice(index, 1)
}
const resetForm = () => {
Object.assign(formData, {
user: { username: '', email: '' },
permissions: { role: '', abilities: [] },
configs: []
})
}
const submitForm = () => {
console.log('Form submitted:', formData)
}
</script>
学习资源
作业
- 实现一个复杂的数据绑定表单,包含嵌套对象和数组
- 创建一个响应式数据管理器,支持性能优化
- 分析 Element Plus Table 组件的数据绑定机制
下一步
明天我们将学习 Element Plus 的生命周期管理与钩子函数应用。