Skip to content

第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>

学习资源

作业

  1. 实现一个复杂的数据绑定表单,包含嵌套对象和数组
  2. 创建一个响应式数据管理器,支持性能优化
  3. 分析 Element Plus Table 组件的数据绑定机制

下一步

明天我们将学习 Element Plus 的生命周期管理与钩子函数应用。

Element Plus Study Guide