Vue3 模板引用 useTemplateRef 详解

Vue3 模板引用 useTemplateRef 详解

核心概念理解

什么是模板引用?

模板引用(Template Ref)是 Vue 提供的一种直接访问 DOM 元素或组件实例的方式。通过 useTemplateRef,我们可以在 Composition API 中获取模板中的元素引用。

为什么需要模板引用?

  • 直接操作 DOM 元素(如聚焦、动画等)
  • 调用子组件的方法
  • 获取元素的尺寸和位置信息
  • 集成第三方库(如图表、地图等)

基础用法

1. 基本模板引用

vue 复制代码
<template>
  <div class="template-ref-demo">
    <h2>基础模板引用</h2>
    
    <!-- 绑定 ref 到元素 -->
    <input 
      ref="inputRef"
      v-model="inputValue"
      placeholder="输入文字试试"
      class="form-input"
    >
    
    <div class="button-group">
      <button @click="focusInput">聚焦输入框</button>
      <button @click="selectInputText">选中文字</button>
      <button @click="clearInput">清空输入</button>
    </div>
    
    <!-- 绑定 ref 到元素 -->
    <div 
      ref="boxRef"
      class="color-box"
      :style="{ backgroundColor: boxColor }"
    >
      点击我改变颜色
    </div>
    
    <div class="box-controls">
      <button @click="changeBoxColor">改变颜色</button>
      <button @click="getBoxInfo">获取盒子信息</button>
    </div>
    
    <div class="info-display">
      <p>输入框值: {{ inputValue }}</p>
      <p>盒子颜色: {{ boxColor }}</p>
      <p v-if="boxInfo">盒子信息: {{ boxInfo }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef } from 'vue'

const inputValue = ref('Hello Vue3!')
const boxColor = ref('#42b983')
const boxInfo = ref('')

// 获取模板引用
const inputRef = useTemplateRef('inputRef')
const boxRef = useTemplateRef('boxRef')

// 操作输入框
const focusInput = () => {
  if (inputRef.value) {
    inputRef.value.focus()
  }
}

const selectInputText = () => {
  if (inputRef.value) {
    inputRef.value.select()
  }
}

const clearInput = () => {
  inputValue.value = ''
  if (inputRef.value) {
    inputRef.value.focus()
  }
}

// 操作盒子
const changeBoxColor = () => {
  const colors = ['#42b983', '#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4']
  const randomColor = colors[Math.floor(Math.random() * colors.length)]
  boxColor.value = randomColor
}

const getBoxInfo = () => {
  if (boxRef.value) {
    const rect = boxRef.value.getBoundingClientRect()
    boxInfo.value = `宽: ${rect.width.toFixed(1)}px, 高: ${rect.height.toFixed(1)}px, X: ${rect.x}, Y: ${rect.y}`
  }
}
</script>

<style>
.template-ref-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.form-input {
  width: 100%;
  padding: 12px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  margin-bottom: 20px;
}

.form-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.button-group, .box-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

button {
  padding: 10px 16px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
}

button:hover {
  background-color: #0056b3;
}

.color-box {
  width: 200px;
  height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
  border-radius: 8px;
  margin: 20px 0;
  cursor: pointer;
  transition: all 0.3s ease;
  user-select: none;
}

.color-box:hover {
  transform: scale(1.05);
}

.info-display {
  padding: 15px;
  background-color: #f8f9fa;
  border-radius: 4px;
  margin-top: 20px;
}

.info-display p {
  margin: 8px 0;
}
</style>

与传统 ref 的对比

1. 传统方式 vs useTemplateRef

vue 复制代码
<template>
  <div class="comparison-demo">
    <h2>传统 ref vs useTemplateRef 对比</h2>
    
    <!-- 传统方式 -->
    <div class="demo-section">
      <h3>传统方式 (ref)</h3>
      <input 
        ref="traditionalRef"
        v-model="traditionalValue"
        placeholder="传统方式输入"
        class="form-input"
      >
      <div class="button-group">
        <button @click="useTraditionalRef">使用传统 ref</button>
      </div>
    </div>
    
    <!-- useTemplateRef 方式 -->
    <div class="demo-section">
      <h3>useTemplateRef 方式</h3>
      <input 
        ref="templateRef"
        v-model="templateValue"
        placeholder="useTemplateRef 方式输入"
        class="form-input"
      >
      <div class="button-group">
        <button @click="useTemplateRefMethod">使用 useTemplateRef</button>
      </div>
    </div>
    
    <!-- 循环中的引用 -->
    <div class="demo-section">
      <h3>循环中的引用</h3>
      <div class="item-list">
        <div 
          v-for="item in items" 
          :key="item.id"
          :ref="`item-${item.id}`"
          class="list-item"
          @click="highlightItem(item.id)"
        >
          {{ item.name }}
        </div>
      </div>
      <div class="button-group">
        <button @click="getAllItems">获取所有项目</button>
        <button @click="clearHighlights">清除高亮</button>
      </div>
    </div>
    
    <div class="result-section">
      <h3>结果展示</h3>
      <p>传统方式值: {{ traditionalValue }}</p>
      <p>useTemplateRef 方式值: {{ templateValue }}</p>
      <p v-if="selectedItem">选中项目: {{ selectedItem }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'

// 传统方式
const traditionalValue = ref('传统方式')
const traditionalRef = ref(null)

// useTemplateRef 方式
const templateValue = ref('useTemplateRef 方式')
const templateRef = useTemplateRef('templateRef')

// 循环数据
const items = ref([
  { id: 1, name: '项目 1' },
  { id: 2, name: '项目 2' },
  { id: 3, name: '项目 3' },
  { id: 4, name: '项目 4' }
])

const selectedItem = ref('')

// 传统方式操作
const useTraditionalRef = () => {
  if (traditionalRef.value) {
    traditionalRef.value.style.backgroundColor = '#d4edda'
    traditionalRef.value.focus()
    setTimeout(() => {
      if (traditionalRef.value) {
        traditionalRef.value.style.backgroundColor = ''
      }
    }, 1000)
  }
}

// useTemplateRef 方式操作
const useTemplateRefMethod = () => {
  if (templateRef.value) {
    templateRef.value.style.backgroundColor = '#cce7ff'
    templateRef.value.focus()
    setTimeout(() => {
      if (templateRef.value) {
        templateRef.value.style.backgroundColor = ''
      }
    }, 1000)
  }
}

// 循环中的引用操作
const highlightItem = (itemId) => {
  // 获取特定项目的引用
  const itemRef = useTemplateRef(`item-${itemId}`)
  if (itemRef.value) {
    itemRef.value.style.backgroundColor = '#fff3cd'
    selectedItem.value = items.value.find(item => item.id === itemId)?.name
  }
}

const getAllItems = () => {
  console.log('获取所有项目引用')
  items.value.forEach(item => {
    const itemRef = useTemplateRef(`item-${item.id}`)
    if (itemRef.value) {
      console.log(`项目 ${item.id}:`, itemRef.value.textContent)
    }
  })
}

const clearHighlights = () => {
  items.value.forEach(item => {
    const itemRef = useTemplateRef(`item-${item.id}`)
    if (itemRef.value) {
      itemRef.value.style.backgroundColor = ''
    }
  })
  selectedItem.value = ''
}

// 组件挂载后演示
onMounted(() => {
  console.log('组件已挂载')
})
</script>

<style>
.comparison-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.demo-section h3 {
  margin-top: 0;
  color: #495057;
}

.form-input {
  width: 100%;
  padding: 12px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  margin: 10px 0;
}

.form-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.button-group {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.item-list {
  margin: 15px 0;
}

.list-item {
  padding: 12px;
  margin: 8px 0;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.list-item:hover {
  background-color: #e9ecef;
  transform: translateX(5px);
}

.result-section {
  padding: 20px;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.result-section h3 {
  margin-top: 0;
  color: #495057;
}
</style>

实际应用场景

1. 表单操作和验证

vue 复制代码
<template>
  <div class="form-ref-demo">
    <h2>表单操作和验证</h2>
    
    <form @submit.prevent="submitForm" class="validation-form">
      <!-- 姓名输入 -->
      <div class="form-group">
        <label for="name">姓名 *</label>
        <input 
          id="name"
          ref="nameRef"
          v-model="formData.name"
          type="text"
          placeholder="请输入姓名"
          :class="{ error: errors.name }"
          @blur="validateName"
        >
        <span v-if="errors.name" class="error-message">{{ errors.name }}</span>
      </div>
      
      <!-- 邮箱输入 -->
      <div class="form-group">
        <label for="email">邮箱 *</label>
        <input 
          id="email"
          ref="emailRef"
          v-model="formData.email"
          type="email"
          placeholder="请输入邮箱"
          :class="{ error: errors.email }"
          @blur="validateEmail"
        >
        <span v-if="errors.email" class="error-message">{{ errors.email }}</span>
      </div>
      
      <!-- 电话输入 -->
      <div class="form-group">
        <label for="phone">电话</label>
        <input 
          id="phone"
          ref="phoneRef"
          v-model="formData.phone"
          type="tel"
          placeholder="请输入电话号码"
          :class="{ error: errors.phone }"
          @blur="validatePhone"
        >
        <span v-if="errors.phone" class="error-message">{{ errors.phone }}</span>
      </div>
      
      <!-- 文本域 -->
      <div class="form-group">
        <label for="message">留言</label>
        <textarea 
          id="message"
          ref="messageRef"
          v-model="formData.message"
          placeholder="请输入留言内容"
          rows="4"
          class="form-textarea"
        ></textarea>
        <div class="char-count">{{ formData.message.length }}/200</div>
      </div>
      
      <!-- 表单控件 -->
      <div class="form-controls">
        <button type="submit" class="submit-btn">提交表单</button>
        <button @click="focusFirstError" type="button" class="focus-btn">定位错误</button>
        <button @click="resetForm" type="button" class="reset-btn">重置表单</button>
      </div>
    </form>
    
    <!-- 提交结果 -->
    <div v-if="submittedData" class="result-section">
      <h3>提交成功!</h3>
      <pre>{{ JSON.stringify(submittedData, null, 2) }}</pre>
      <button @click="submittedData = null" class="reset-btn">重新填写</button>
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef, reactive } from 'vue'

// 表单数据
const formData = reactive({
  name: '',
  email: '',
  phone: '',
  message: ''
})

// 错误信息
const errors = reactive({
  name: '',
  email: '',
  phone: ''
})

const submittedData = ref(null)

// 获取模板引用
const nameRef = useTemplateRef('nameRef')
const emailRef = useTemplateRef('emailRef')
const phoneRef = useTemplateRef('phoneRef')
const messageRef = useTemplateRef('messageRef')

// 验证函数
const validateName = () => {
  if (!formData.name.trim()) {
    errors.name = '姓名不能为空'
  } else if (formData.name.length < 2) {
    errors.name = '姓名至少2个字符'
  } else {
    errors.name = ''
  }
}

const validateEmail = () => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!formData.email.trim()) {
    errors.email = '邮箱不能为空'
  } else if (!emailRegex.test(formData.email)) {
    errors.email = '请输入有效的邮箱地址'
  } else {
    errors.email = ''
  }
}

const validatePhone = () => {
  if (formData.phone && !/^1[3-9]\d{9}$/.test(formData.phone)) {
    errors.phone = '请输入有效的手机号码'
  } else {
    errors.phone = ''
  }
}

// 聚焦到第一个错误字段
const focusFirstError = () => {
  if (errors.name && nameRef.value) {
    nameRef.value.focus()
    nameRef.value.scrollIntoView({ behavior: 'smooth', block: 'center' })
  } else if (errors.email && emailRef.value) {
    emailRef.value.focus()
    emailRef.value.scrollIntoView({ behavior: 'smooth', block: 'center' })
  } else if (errors.phone && phoneRef.value) {
    phoneRef.value.focus()
    phoneRef.value.scrollIntoView({ behavior: 'smooth', block: 'center' })
  } else {
    alert('没有发现错误字段')
  }
}

// 重置表单
const resetForm = () => {
  Object.assign(formData, {
    name: '',
    email: '',
    phone: '',
    message: ''
  })
  
  Object.keys(errors).forEach(key => {
    errors[key] = ''
  })
  
  // 聚焦到第一个输入框
  setTimeout(() => {
    if (nameRef.value) {
      nameRef.value.focus()
    }
  }, 100)
}

// 提交表单
const submitForm = () => {
  // 验证所有字段
  validateName()
  validateEmail()
  validatePhone()
  
  // 检查是否有错误
  const hasErrors = Object.values(errors).some(error => error)
  const hasEmptyRequired = !formData.name.trim() || !formData.email.trim()
  
  if (hasErrors || hasEmptyRequired) {
    focusFirstError()
    return
  }
  
  // 提交成功
  submittedData.value = { ...formData }
  console.log('表单提交:', formData)
}
</script>

<style>
.form-ref-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.validation-form {
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 8px;
  padding: 25px;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  font-weight: bold;
  color: #495057;
}

.form-group input, .form-textarea {
  width: 100%;
  padding: 12px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  transition: border-color 0.2s ease;
}

.form-group input:focus, .form-textarea:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.form-group input.error {
  border-color: #dc3545;
}

.form-textarea {
  resize: vertical;
  font-family: inherit;
}

.char-count {
  text-align: right;
  font-size: 14px;
  color: #6c757d;
  margin-top: 5px;
}

.error-message {
  color: #dc3545;
  font-size: 14px;
  margin-top: 5px;
  display: block;
}

.form-controls {
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
  margin-top: 30px;
}

.submit-btn, .focus-btn, .reset-btn {
  padding: 12px 20px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.submit-btn {
  background-color: #28a745;
  color: white;
  flex: 1;
}

.submit-btn:hover {
  background-color: #218838;
}

.focus-btn {
  background-color: #ffc107;
  color: #212529;
}

.focus-btn:hover {
  background-color: #e0a800;
}

.reset-btn {
  background-color: #6c757d;
  color: white;
}

.reset-btn:hover {
  background-color: #5a6268;
}

.result-section {
  padding: 20px;
  background-color: #d4edda;
  border: 1px solid #c3e6cb;
  border-radius: 8px;
}

.result-section h3 {
  color: #155724;
  margin-top: 0;
}

.result-section pre {
  background-color: #fff;
  padding: 15px;
  border-radius: 4px;
  overflow-x: auto;
  margin: 15px 0;
  font-size: 14px;
}
</style>

2. 动画和交互效果

vue 复制代码
<template>
  <div class="animation-demo">
    <h2>动画和交互效果</h2>
    
    <!-- 拖拽区域 -->
    <div class="drag-section">
      <h3>拖拽功能</h3>
      <div 
        ref="dragAreaRef"
        class="drag-area"
        @mousedown="startDrag"
        @touchstart="startDrag"
      >
        <div 
          ref="draggableBoxRef"
          class="draggable-box"
          :style="{
            left: boxPosition.x + 'px',
            top: boxPosition.y + 'px',
            cursor: isDragging ? 'grabbing' : 'grab'
          }"
        >
          拖拽我
        </div>
      </div>
      <div class="position-info">
        位置: X: {{ boxPosition.x }}, Y: {{ boxPosition.y }}
      </div>
    </div>
    
    <!-- 滚动同步 -->
    <div class="scroll-section">
      <h3>滚动同步</h3>
      <div class="scroll-container">
        <div 
          ref="scrollContainer1Ref"
          class="scroll-box"
          @scroll="syncScroll"
        >
          <div class="scroll-content" style="height: 500px;">
            <div 
              v-for="item in 50" 
              :key="item"
              class="scroll-item"
            >
              内容项目 {{ item }}
            </div>
          </div>
        </div>
        
        <div 
          ref="scrollContainer2Ref"
          class="scroll-box"
          @scroll="syncScroll"
        >
          <div class="scroll-content" style="height: 500px;">
            <div 
              v-for="item in 50" 
              :key="item"
              class="scroll-item"
            >
              同步项目 {{ item }}
            </div>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 焦点管理 -->
    <div class="focus-section">
      <h3>焦点管理</h3>
      <div class="focus-demo">
        <input 
          ref="focusInput1Ref"
          v-model="focusValues.input1"
          placeholder="输入框 1"
          class="focus-input"
          @keydown="handleKeyNavigation"
        >
        <input 
          ref="focusInput2Ref"
          v-model="focusValues.input2"
          placeholder="输入框 2"
          class="focus-input"
          @keydown="handleKeyNavigation"
        >
        <input 
          ref="focusInput3Ref"
          v-model="focusValues.input3"
          placeholder="输入框 3"
          class="focus-input"
          @keydown="handleKeyNavigation"
        >
        <textarea 
          ref="focusTextareaRef"
          v-model="focusValues.textarea"
          placeholder="文本域"
          rows="3"
          class="focus-textarea"
          @keydown="handleKeyNavigation"
        ></textarea>
      </div>
      <div class="focus-controls">
        <button @click="focusNext">下一个</button>
        <button @click="focusPrevious">上一个</button>
        <button @click="focusFirst">第一个</button>
        <button @click="focusLast">最后一个</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef, reactive } from 'vue'

// 拖拽相关
const boxPosition = ref({ x: 50, y: 50 })
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })

const dragAreaRef = useTemplateRef('dragAreaRef')
const draggableBoxRef = useTemplateRef('draggableBoxRef')

// 滚动相关
const scrollContainer1Ref = useTemplateRef('scrollContainer1Ref')
const scrollContainer2Ref = useTemplateRef('scrollContainer2Ref')

// 焦点相关
const focusValues = reactive({
  input1: '',
  input2: '',
  input3: '',
  textarea: ''
})

const focusInput1Ref = useTemplateRef('focusInput1Ref')
const focusInput2Ref = useTemplateRef('focusInput2Ref')
const focusInput3Ref = useTemplateRef('focusInput3Ref')
const focusTextareaRef = useTemplateRef('focusTextareaRef')

const focusRefs = [
  focusInput1Ref,
  focusInput2Ref,
  focusInput3Ref,
  focusTextareaRef
]

// 拖拽功能
const startDrag = (event) => {
  isDragging.value = true
  const clientX = event.clientX || event.touches[0].clientX
  const clientY = event.clientY || event.touches[0].clientY
  
  dragStart.value = {
    x: clientX - boxPosition.value.x,
    y: clientY - boxPosition.value.y
  }
  
  document.addEventListener('mousemove', drag)
  document.addEventListener('touchmove', drag)
  document.addEventListener('mouseup', stopDrag)
  document.addEventListener('touchend', stopDrag)
}

const drag = (event) => {
  if (!isDragging.value || !dragAreaRef.value) return
  
  const clientX = event.clientX || event.touches[0].clientX
  const clientY = event.clientY || event.touches[0].clientY
  
  const rect = dragAreaRef.value.getBoundingClientRect()
  const maxX = rect.width - 100
  const maxY = rect.height - 100
  
  boxPosition.value = {
    x: Math.max(0, Math.min(maxX, clientX - dragStart.value.x - rect.left)),
    y: Math.max(0, Math.min(maxY, clientY - dragStart.value.y - rect.top))
  }
}

const stopDrag = () => {
  isDragging.value = false
  document.removeEventListener('mousemove', drag)
  document.removeEventListener('touchmove', drag)
  document.removeEventListener('mouseup', stopDrag)
  document.removeEventListener('touchend', stopDrag)
}

// 滚动同步
const syncScroll = (event) => {
  const scrollTop = event.target.scrollTop
  const scrollLeft = event.target.scrollLeft
  
  if (event.target === scrollContainer1Ref.value && scrollContainer2Ref.value) {
    scrollContainer2Ref.value.scrollTop = scrollTop
    scrollContainer2Ref.value.scrollLeft = scrollLeft
  } else if (event.target === scrollContainer2Ref.value && scrollContainer1Ref.value) {
    scrollContainer1Ref.value.scrollTop = scrollTop
    scrollContainer1Ref.value.scrollLeft = scrollLeft
  }
}

// 焦点管理
const handleKeyNavigation = (event) => {
  if (event.key === 'Tab') {
    // Tab 键已经在浏览器中处理了焦点切换
    return
  }
  
  if (event.key === 'ArrowDown' || event.key === 'Enter') {
    event.preventDefault()
    focusNext()
  } else if (event.key === 'ArrowUp') {
    event.preventDefault()
    focusPrevious()
  }
}

const focusNext = () => {
  const currentIndex = focusRefs.findIndex(ref => 
    ref.value === document.activeElement
  )
  
  const nextIndex = (currentIndex + 1) % focusRefs.length
  if (focusRefs[nextIndex].value) {
    focusRefs[nextIndex].value.focus()
  }
}

const focusPrevious = () => {
  const currentIndex = focusRefs.findIndex(ref => 
    ref.value === document.activeElement
  )
  
  const prevIndex = (currentIndex - 1 + focusRefs.length) % focusRefs.length
  if (focusRefs[prevIndex].value) {
    focusRefs[prevIndex].value.focus()
  }
}

const focusFirst = () => {
  if (focusRefs[0].value) {
    focusRefs[0].value.focus()
  }
}

const focusLast = () => {
  const lastIndex = focusRefs.length - 1
  if (focusRefs[lastIndex].value) {
    focusRefs[lastIndex].value.focus()
  }
}
</script>

<style>
.animation-demo {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

.drag-section, .scroll-section, .focus-section {
  margin-bottom: 40px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.drag-section h3, .scroll-section h3, .focus-section h3 {
  margin-top: 0;
  color: #495057;
}

.drag-area {
  width: 100%;
  height: 300px;
  background-color: #e3f2fd;
  border: 2px dashed #2196f3;
  border-radius: 8px;
  position: relative;
  overflow: hidden;
  margin-bottom: 15px;
}

.draggable-box {
  position: absolute;
  width: 100px;
  height: 100px;
  background-color: #ff6b6b;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
  user-select: none;
  transition: none;
}

.position-info {
  text-align: center;
  font-weight: bold;
  color: #007bff;
}

.scroll-container {
  display: flex;
  gap: 20px;
  margin-bottom: 15px;
}

.scroll-box {
  flex: 1;
  height: 300px;
  overflow: auto;
  border: 1px solid #ced4da;
  border-radius: 4px;
  background-color: #fff;
}

.scroll-content {
  padding: 10px;
}

.scroll-item {
  padding: 10px;
  margin: 5px 0;
  background-color: #f8f9fa;
  border-radius: 4px;
  border: 1px solid #dee2e6;
}

.focus-demo {
  margin-bottom: 20px;
}

.focus-input, .focus-textarea {
  width: 100%;
  padding: 12px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  margin-bottom: 10px;
}

.focus-input:focus, .focus-textarea:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.focus-textarea {
  resize: vertical;
  font-family: inherit;
}

.focus-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.focus-controls button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
}

.focus-controls button:hover {
  background-color: #0056b3;
}
</style>

与子组件的交互

1. 调用子组件方法

vue 复制代码
<!-- ParentComponent.vue -->
<template>
  <div class="parent-demo">
    <h2>与子组件交互</h2>
    
    <!-- 子组件引用 -->
    <div class="component-section">
      <h3>图表组件</h3>
      <ChartComponent 
        ref="chartRef"
        :data="chartData"
        width="400"
        height="300"
      />
      <div class="chart-controls">
        <button @click="updateChartData">更新数据</button>
        <button @click="animateChart">动画效果</button>
        <button @click="resetChart">重置图表</button>
        <button @click="exportChart">导出图表</button>
      </div>
    </div>
    
    <!-- 表格组件 -->
    <div class="component-section">
      <h3>表格组件</h3>
      <TableComponent 
        ref="tableRef"
        :data="tableData"
        :columns="tableColumns"
      />
      <div class="table-controls">
        <button @click="addTableRow">添加行</button>
        <button @click="deleteSelectedRows">删除选中行</button>
        <button @click="selectAllRows">全选</button>
        <button @click="clearSelection">清空选择</button>
      </div>
    </div>
    
    <!-- 状态显示 -->
    <div class="status-section">
      <h3>组件状态</h3>
      <div class="status-grid">
        <div class="status-item">
          <span>图表状态:</span>
          <span>{{ chartStatus }}</span>
        </div>
        <div class="status-item">
          <span>表格状态:</span>
          <span>{{ tableStatus }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef, reactive } from 'vue'
import ChartComponent from './ChartComponent.vue'
import TableComponent from './TableComponent.vue'

// 图表相关
const chartRef = useTemplateRef('chartRef')
const chartStatus = ref('就绪')
const chartData = ref([
  { name: 'Jan', value: 400 },
  { name: 'Feb', value: 300 },
  { name: 'Mar', value: 200 },
  { name: 'Apr', value: 278 },
  { name: 'May', value: 189 }
])

// 表格相关
const tableRef = useTemplateRef('tableRef')
const tableStatus = ref('就绪')
const tableData = ref([
  { id: 1, name: '张三', age: 25, city: '北京' },
  { id: 2, name: '李四', age: 30, city: '上海' },
  { id: 3, name: '王五', age: 28, city: '广州' }
])

const tableColumns = ref([
  { key: 'id', title: 'ID' },
  { key: 'name', title: '姓名' },
  { key: 'age', title: '年龄' },
  { key: 'city', title: '城市' }
])

// 图表操作
const updateChartData = () => {
  if (chartRef.value) {
    const newData = chartData.value.map(item => ({
      ...item,
      value: Math.floor(Math.random() * 500) + 100
    }))
    chartData.value = newData
    chartStatus.value = '数据已更新'
    
    setTimeout(() => {
      chartStatus.value = '就绪'
    }, 2000)
  }
}

const animateChart = () => {
  if (chartRef.value && chartRef.value.animate) {
    chartRef.value.animate()
    chartStatus.value = '动画播放中'
  }
}

const resetChart = () => {
  if (chartRef.value && chartRef.value.reset) {
    chartRef.value.reset()
    chartStatus.value = '图表已重置'
    
    setTimeout(() => {
      chartStatus.value = '就绪'
    }, 2000)
  }
}

const exportChart = () => {
  if (chartRef.value && chartRef.value.export) {
    const result = chartRef.value.export()
    chartStatus.value = `导出成功: ${result}`
    
    setTimeout(() => {
      chartStatus.value = '就绪'
    }, 2000)
  }
}

// 表格操作
const addTableRow = () => {
  if (tableRef.value) {
    const newId = Math.max(...tableData.value.map(item => item.id)) + 1
    tableData.value.push({
      id: newId,
      name: `用户${newId}`,
      age: Math.floor(Math.random() * 30) + 20,
      city: ['北京', '上海', '广州', '深圳', '杭州'][Math.floor(Math.random() * 5)]
    })
    tableStatus.value = '已添加新行'
    
    setTimeout(() => {
      tableStatus.value = '就绪'
    }, 2000)
  }
}

const deleteSelectedRows = () => {
  if (tableRef.value && tableRef.value.getSelectedRows) {
    const selected = tableRef.value.getSelectedRows()
    if (selected.length > 0) {
      tableData.value = tableData.value.filter(
        item => !selected.includes(item.id)
      )
      tableStatus.value = `已删除 ${selected.length} 行`
      
      setTimeout(() => {
        tableStatus.value = '就绪'
      }, 2000)
    } else {
      tableStatus.value = '请选择要删除的行'
    }
  }
}

const selectAllRows = () => {
  if (tableRef.value && tableRef.value.selectAll) {
    tableRef.value.selectAll()
    tableStatus.value = '已全选'
  }
}

const clearSelection = () => {
  if (tableRef.value && tableRef.value.clearSelection) {
    tableRef.value.clearSelection()
    tableStatus.value = '已清空选择'
  }
}
</script>

<style>
.parent-demo {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

.component-section {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.component-section h3 {
  margin-top: 0;
  color: #495057;
}

.chart-controls, .table-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 15px;
}

button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
}

button:hover {
  background-color: #0056b3;
}

.status-section {
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.status-section h3 {
  margin-top: 0;
  color: #495057;
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 15px;
}

.status-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  background-color: #fff;
  border-radius: 4px;
  border: 1px solid #dee2e6;
}

.status-item span:first-child {
  font-weight: bold;
  color: #495057;
}
</style>
vue 复制代码
<!-- ChartComponent.vue -->
<template>
  <div class="chart-component">
    <svg 
      :width="width" 
      :height="height"
      class="chart-svg"
    >
      <!-- 简单的柱状图 -->
      <rect 
        v-for="(item, index) in data" 
        :key="index"
        :x="index * (width / data.length) + 20"
        :y="height - (item.value / maxValue) * (height - 50) - 20"
        :width="(width / data.length) - 40"
        :height="(item.value / maxValue) * (height - 50)"
        :fill="`hsl(${index * 60}, 70%, 50%)`"
        class="chart-bar"
      />
      
      <!-- 标签 -->
      <text 
        v-for="(item, index) in data" 
        :key="'label-' + index"
        :x="index * (width / data.length) + (width / data.length / 2)"
        :y="height - 5"
        text-anchor="middle"
        font-size="12"
        fill="#333"
      >
        {{ item.name }}
      </text>
    </svg>
  </div>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  data: {
    type: Array,
    required: true
  },
  width: {
    type: [Number, String],
    default: 400
  },
  height: {
    type: [Number, String],
    default: 300
  }
})

const maxValue = computed(() => {
  return Math.max(...props.data.map(item => item.value), 1)
})

// 子组件暴露的方法
defineExpose({
  animate() {
    console.log('图表动画播放')
    // 这里可以实现动画逻辑
  },
  
  reset() {
    console.log('图表重置')
    // 这里可以实现重置逻辑
  },
  
  export() {
    console.log('图表导出')
    return 'chart-export.png'
  }
})
</script>

<style>
.chart-component {
  display: inline-block;
  border: 1px solid #dee2e6;
  border-radius: 4px;
  background-color: #fff;
}

.chart-svg {
  display: block;
}

.chart-bar {
  transition: all 0.3s ease;
}

.chart-bar:hover {
  opacity: 0.8;
  transform: scale(1.05);
}
</style>
vue 复制代码
<!-- TableComponent.vue -->
<template>
  <div class="table-component">
    <table class="data-table">
      <thead>
        <tr>
          <th>
            <input 
              type="checkbox" 
              :checked="isAllSelected"
              @change="toggleAllSelection"
            >
          </th>
          <th 
            v-for="column in columns" 
            :key="column.key"
          >
            {{ column.title }}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr 
          v-for="row in data" 
          :key="row.id"
          :class="{ selected: selectedRows.includes(row.id) }"
          @click="toggleRowSelection(row.id)"
        >
          <td>
            <input 
              type="checkbox" 
              :checked="selectedRows.includes(row.id)"
              @click.stop="toggleRowSelection(row.id)"
            >
          </td>
          <td v-for="column in columns" :key="column.key">
            {{ row[column.key] }}
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  data: {
    type: Array,
    required: true
  },
  columns: {
    type: Array,
    required: true
  }
})

const selectedRows = ref([])

const isAllSelected = computed(() => {
  return selectedRows.value.length > 0 && 
         selectedRows.value.length === props.data.length
})

const toggleRowSelection = (rowId) => {
  const index = selectedRows.value.indexOf(rowId)
  if (index > -1) {
    selectedRows.value.splice(index, 1)
  } else {
    selectedRows.value.push(rowId)
  }
}

const toggleAllSelection = () => {
  if (isAllSelected.value) {
    selectedRows.value = []
  } else {
    selectedRows.value = props.data.map(item => item.id)
  }
}

// 子组件暴露的方法
defineExpose({
  getSelectedRows() {
    return [...selectedRows.value]
  },
  
  selectAll() {
    selectedRows.value = props.data.map(item => item.id)
  },
  
  clearSelection() {
    selectedRows.value = []
  }
})
</script>

<style>
.table-component {
  overflow-x: auto;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  background-color: #fff;
}

.data-table th,
.data-table td {
  padding: 12px;
  text-align: left;
  border: 1px solid #dee2e6;
}

.data-table th {
  background-color: #f8f9fa;
  font-weight: bold;
}

.data-table tbody tr:hover {
  background-color: #f8f9fa;
}

.data-table tbody tr.selected {
  background-color: #d1ecf1;
}

.data-table input[type="checkbox"] {
  margin: 0;
  cursor: pointer;
}
</style>

注意事项和最佳实践

1. 生命周期和访问时机

vue 复制代码
<template>
  <div class="lifecycle-demo">
    <h2>生命周期和访问时机</h2>
    
    <div class="demo-section">
      <h3>访问时机测试</h3>
      <input 
        ref="testRef"
        v-model="testValue"
        placeholder="测试输入框"
        class="form-input"
      >
      
      <div class="button-group">
        <button @click="testImmediateAccess">立即访问</button>
        <button @click="testNextTickAccess">nextTick 访问</button>
        <button @click="testMountedAccess">挂载后访问</button>
        <button @click="testUpdatedAccess">更新后访问</button>
      </div>
      
      <div class="log-section">
        <h4>访问日志:</h4>
        <ul class="log-list">
          <li v-for="(log, index) in accessLogs" :key="index">
            {{ log }}
          </li>
        </ul>
      </div>
    </div>
    
    <!-- 条件渲染测试 -->
    <div class="demo-section">
      <h3>条件渲染测试</h3>
      <label class="checkbox-label">
        <input 
          v-model="showElement" 
          type="checkbox"
        >
        显示元素
      </label>
      
      <div v-if="showElement" class="conditional-element">
        <input 
          ref="conditionalRef"
          v-model="conditionalValue"
          placeholder="条件渲染的输入框"
          class="form-input"
        >
        <button @click="accessConditionalRef">访问条件引用</button>
      </div>
      
      <p>条件引用状态: {{ conditionalRefStatus }}</p>
    </div>
    
    <!-- 循环引用测试 -->
    <div class="demo-section">
      <h3>循环引用测试</h3>
      <div class="list-controls">
        <button @click="addItem">添加项目</button>
        <button @click="removeItem">删除项目</button>
        <button @click="accessAllRefs">访问所有引用</button>
      </div>
      
      <div class="item-list">
        <div 
          v-for="item in listItems" 
          :key="item.id"
          class="list-item"
        >
          <input 
            :ref="`item-${item.id}`"
            v-model="item.value"
            :placeholder="`项目 ${item.id}`"
            class="form-input small"
          >
          <span>项目 {{ item.id }}</span>
        </div>
      </div>
      
      <p>列表项目数: {{ listItems.length }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef, onMounted, onUpdated, nextTick } from 'vue'

// 基础测试
const testValue = ref('测试值')
const testRef = useTemplateRef('testRef')
const accessLogs = ref([])

const listItems = ref([
  { id: 1, value: '项目1' },
  { id: 2, value: '项目2' }
])

// 条件渲染
const showElement = ref(true)
const conditionalValue = ref('条件值')
const conditionalRef = useTemplateRef('conditionalRef')
const conditionalRefStatus = ref('未访问')

// 立即访问(可能为 null)
const testImmediateAccess = () => {
  const result = testRef.value ? '成功' : '失败(null)'
  accessLogs.value.push(`立即访问: ${result} - ${new Date().toLocaleTimeString()}`)
  console.log('立即访问:', testRef.value)
}

// nextTick 访问
const testNextTickAccess = () => {
  nextTick(() => {
    const result = testRef.value ? '成功' : '失败(null)'
    accessLogs.value.push(`nextTick 访问: ${result} - ${new Date().toLocaleTimeString()}`)
    console.log('nextTick 访问:', testRef.value)
  })
}

// 挂载后访问
const testMountedAccess = () => {
  const result = testRef.value ? '成功' : '失败(null)'
  accessLogs.value.push(`挂载后访问: ${result} - ${new Date().toLocaleTimeString()}`)
  console.log('挂载后访问:', testRef.value)
}

// 更新后访问
const testUpdatedAccess = () => {
  const result = testRef.value ? '成功' : '失败(null)'
  accessLogs.value.push(`更新后访问: ${result} - ${new Date().toLocaleTimeString()}`)
  console.log('更新后访问:', testRef.value)
}

// 条件引用访问
const accessConditionalRef = () => {
  if (showElement.value && conditionalRef.value) {
    conditionalRef.value.focus()
    conditionalRefStatus.value = `成功访问 - 值: ${conditionalValue.value}`
  } else {
    conditionalRefStatus.value = '无法访问(元素未显示或引用为空)'
  }
}

// 循环引用操作
const addItem = () => {
  const newId = listItems.value.length > 0 
    ? Math.max(...listItems.value.map(item => item.id)) + 1 
    : 1
  listItems.value.push({ id: newId, value: `项目${newId}` })
}

const removeItem = () => {
  if (listItems.value.length > 0) {
    listItems.value.pop()
  }
}

const accessAllRefs = () => {
  console.log('访问所有循环引用:')
  listItems.value.forEach(item => {
    const itemRef = useTemplateRef(`item-${item.id}`)
    if (itemRef.value) {
      console.log(`项目 ${item.id}:`, itemRef.value.value)
    } else {
      console.log(`项目 ${item.id}: 引用为空`)
    }
  })
}

// 生命周期钩子
onMounted(() => {
  accessLogs.value.push(`组件挂载完成 - ${new Date().toLocaleTimeString()}`)
  console.log('组件挂载完成,引用应该可用:', testRef.value)
})

onUpdated(() => {
  accessLogs.value.push(`组件更新完成 - ${new Date().toLocaleTimeString()}`)
  console.log('组件更新完成')
})
</script>

<style>
.lifecycle-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.demo-section h3 {
  margin-top: 0;
  color: #495057;
}

.form-input {
  width: 100%;
  padding: 12px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  margin: 10px 0;
}

.form-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.form-input.small {
  width: 150px;
  display: inline-block;
  margin-right: 10px;
}

.button-group {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin: 15px 0;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 15px 0;
  cursor: pointer;
  user-select: none;
}

.checkbox-label input {
  width: 18px;
  height: 18px;
  cursor: pointer;
}

.conditional-element {
  padding: 15px;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 4px;
  margin: 15px 0;
}

.list-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 15px;
}

.item-list {
  margin: 15px 0;
}

.list-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  margin: 8px 0;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 4px;
}

.log-section {
  margin-top: 20px;
}

.log-section h4 {
  margin-top: 0;
  color: #495057;
}

.log-list {
  max-height: 200px;
  overflow-y: auto;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 4px;
  padding: 10px;
  margin: 0;
  font-size: 14px;
}

.log-list li {
  padding: 3px 0;
  border-bottom: 1px solid #eee;
}

.log-list li:last-child {
  border-bottom: none;
}

button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
}

button:hover {
  background-color: #0056b3;
}
</style>

2. 常见陷阱和解决方案

vue 复制代码
<template>
  <div class="pitfalls-demo">
    <h2>常见陷阱和解决方案</h2>
    
    <!-- 陷阱1: 异步组件引用 -->
    <div class="demo-section">
      <h3>陷阱1: 异步组件引用</h3>
      <div class="async-demo">
        <button @click="loadAsyncComponent">加载异步组件</button>
        <div v-if="showAsyncComponent" class="async-container">
          <AsyncComponent ref="asyncRef" />
        </div>
        <button 
          @click="callAsyncMethod" 
          :disabled="!showAsyncComponent"
          class="call-btn"
        >
          调用异步方法
        </button>
        <p>异步状态: {{ asyncStatus }}</p>
      </div>
    </div>
    
    <!-- 陷阱2: 响应式更新问题 -->
    <div class="demo-section">
      <h3>陷阱2: 响应式更新问题</h3>
      <div class="reactivity-demo">
        <input 
          ref="reactiveRef"
          v-model="reactiveValue"
          placeholder="响应式输入"
          class="form-input"
        >
        <div class="button-group">
          <button @click="changeReactiveValue">改变值</button>
          <button @click="directDOMChange">直接 DOM 操作</button>
          <button @click="syncReactiveValue">同步响应式值</button>
        </div>
        <p>响应式值: {{ reactiveValue }}</p>
        <p>DOM 值: <span ref="domValueRef"></span></p>
      </div>
    </div>
    
    <!-- 陷阱3: 内存泄漏防护 -->
    <div class="demo-section">
      <h3>陷阱3: 内存泄漏防护</h3>
      <div class="memory-demo">
        <div class="counter-controls">
          <button @click="incrementCounter">增加计数器</button>
          <button @click="decrementCounter">减少计数器</button>
        </div>
        <div class="counter-display">
          <div 
            v-for="n in counter" 
            :key="n"
            class="counter-item"
          >
            <input 
              :ref="`counter-${n}`"
              :value="`计数器${n}`"
              class="form-input small"
            >
          </div>
        </div>
        <p>当前计数器数: {{ counter }}</p>
        <button @click="cleanupRefs">清理引用</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, useTemplateRef, onMounted, onUnmounted } from 'vue'

// 异步组件相关
const showAsyncComponent = ref(false)
const asyncRef = useTemplateRef('asyncRef')
const asyncStatus = ref('未加载')

const loadAsyncComponent = () => {
  showAsyncComponent.value = true
  asyncStatus.value = '组件加载中...'
  
  // 模拟异步加载
  setTimeout(() => {
    asyncStatus.value = '组件已加载'
  }, 1000)
}

const callAsyncMethod = () => {
  // 安全调用异步组件方法
  if (asyncRef.value && typeof asyncRef.value.someMethod === 'function') {
    const result = asyncRef.value.someMethod()
    asyncStatus.value = `方法调用结果: ${result}`
  } else {
    asyncStatus.value = '方法不可用'
  }
}

// 响应式更新相关
const reactiveValue = ref('初始值')
const reactiveRef = useTemplateRef('reactiveRef')
const domValueRef = useTemplateRef('domValueRef')

const changeReactiveValue = () => {
  reactiveValue.value = `更新值 ${Date.now()}`
}

const directDOMChange = () => {
  if (reactiveRef.value) {
    reactiveRef.value.value = `DOM 直接修改 ${Date.now()}`
    // 注意:这不会更新 reactiveValue
  }
}

const syncReactiveValue = () => {
  if (reactiveRef.value) {
    reactiveValue.value = reactiveRef.value.value
  }
}

onMounted(() => {
  // 更新 DOM 显示
  if (domValueRef.value) {
    domValueRef.value.textContent = reactiveValue.value
  }
})

// 监听响应式值变化
import { watch } from 'vue'
watch(reactiveValue, (newVal) => {
  if (domValueRef.value) {
    domValueRef.value.textContent = newVal
  }
})

// 内存泄漏防护
const counter = ref(3)

const incrementCounter = () => {
  counter.value++
}

const decrementCounter = () => {
  if (counter.value > 0) {
    counter.value--
  }
}

const cleanupRefs = () => {
  console.log('清理引用')
  // Vue 会自动管理引用的清理
  // 这里只是演示概念
}

// 组件卸载时的清理
onUnmounted(() => {
  console.log('组件卸载,清理资源')
  // 清理定时器、事件监听器等
})
</script>

<style>
.pitfalls-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.demo-section h3 {
  margin-top: 0;
  color: #495057;
}

.async-demo, .reactivity-demo, .memory-demo {
  margin: 15px 0;
}

.form-input {
  width: 100%;
  padding: 12px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  margin: 10px 0;
}

.form-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.form-input.small {
  width: 200px;
  display: inline-block;
}

.button-group {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin: 15px 0;
}

.call-btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #28a745;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
  margin: 5px;
}

.call-btn:hover:not(:disabled) {
  background-color: #218838;
}

.call-btn:disabled {
  background-color: #6c757d;
  cursor: not-allowed;
}

.counter-controls {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}

.counter-display {
  margin: 15px 0;
  max-height: 200px;
  overflow-y: auto;
  padding: 10px;
  background-color: #f8f9fa;
  border-radius: 4px;
}

.counter-item {
  margin: 8px 0;
  padding: 8px;
  background-color: #fff;
  border: 1px solid #dee2e6;
  border-radius: 4px;
}

button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  background-color: #007bff;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
  margin: 5px;
}

button:hover {
  background-color: #0056b3;
}
</style>
vue 复制代码
<!-- AsyncComponent.vue -->
<template>
  <div class="async-component">
    <h4>异步加载的组件</h4>
    <p>这是一个动态加载的组件</p>
    <div class="async-content">
      加载时间: {{ loadTime }}
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const loadTime = ref(new Date().toLocaleTimeString())

// 暴露方法给父组件调用
defineExpose({
  someMethod() {
    return `异步方法调用成功 - ${new Date().toLocaleTimeString()}`
  },
  
  getData() {
    return {
      message: '来自异步组件的数据',
      timestamp: Date.now()
    }
  }
})
</script>

<style>
.async-component {
  padding: 20px;
  background-color: #d1ecf1;
  border: 1px solid #bee5eb;
  border-radius: 8px;
  margin: 15px 0;
}

.async-component h4 {
  margin-top: 0;
  color: #0c5460;
}

.async-content {
  font-style: italic;
  color: #0c5460;
}
</style>

总结

useTemplateRef 的优势

  1. 类型安全:更好的 TypeScript 支持
  2. 明确性:更清晰的引用获取方式
  3. 一致性:与 Composition API 风格一致

使用建议

  1. 访问时机

    • onMounted 后访问引用
    • 使用 nextTick 确保 DOM 更新完成
    • 条件渲染的元素要检查存在性
  2. 性能考虑

    • 避免频繁访问 DOM
    • 及时清理事件监听器
    • 合理使用防抖和节流
  3. 最佳实践

    • 优先使用 Vue 的响应式系统
    • 只在必要时使用模板引用
    • 做好错误处理和边界检查

记忆口诀

  • useTemplateRef:获取模板引用新方式
  • ref="name":模板中标记引用名称
  • useTemplateRef('name'):代码中获取引用
  • onMounted:访问引用的最佳时机
  • 谨慎使用:只在必要时操作 DOM

这样就能很好地掌握 Vue3 的模板引用功能了!

相关推荐
布列瑟农的星空5 分钟前
大话设计模式——关注点分离原则下的事件处理
前端·后端·架构
yvvvy24 分钟前
前端必懂的 Cache 缓存机制详解
前端
北海几经夏39 分钟前
React自定义Hook
前端·react.js
龙在天43 分钟前
从代码到屏幕,浏览器渲染网页做了什么❓
前端
TimelessHaze1 小时前
【performance面试考点】让面试官眼前一亮的performance性能优化
前端·性能优化·trae
yes or ok1 小时前
前端工程师面试题-vue
前端·javascript·vue.js
我要成为前端高手1 小时前
给不支持摇树的三方库(phaser) tree-shake?
前端·javascript
Noxi_lumors1 小时前
VITE BALABALA require balabla not supported
前端·vite
周胜21 小时前
node-sass
前端
aloha_2 小时前
Windows 系统中,杀死占用某个端口(如 8080)的进程
前端