Vue.js 模板引用

模板引用(Template Refs) 是 Vue.js 中用于直接访问 DOM 元素或子组件实例的特殊特性。通过 ref 属性,你可以在组件中获取对特定元素的引用,从而执行 DOM 操作或调用子组件方法。

1. 什么是模板引用?

模板引用是 Vue.js 提供的一种机制,允许你在 Vue 实例中直接引用模板中的 DOM 元素或子组件实例。这是通过 ref 属性实现的。

1
添加ref属性

在模板元素上添加 ref 属性

2
引用自动注册

Vue自动将引用添加到 $refs 对象

3
访问引用

通过 this.$refs.refName 访问

模板中: <input ref="usernameInput">
↓ Vue自动注册
组件实例中: this.$refs.usernameInput ← 访问DOM元素

为什么需要模板引用?

在大多数情况下,你应该避免直接操作 DOM,因为 Vue 的声明式渲染和数据绑定已经处理了大部分 DOM 操作。然而,有时你可能需要:

需要直接操作DOM
  • 管理输入框焦点
  • 触发元素点击事件
  • 获取元素尺寸和位置
  • 集成第三方DOM库
  • 执行DOM动画
需要访问子组件
  • 调用子组件的方法
  • 访问子组件的数据
  • 直接操作子组件的DOM
  • 集成非Vue的组件
  • 执行子组件的特定操作
重要: 模板引用是"逃生舱口"(escape hatch),应该在确实需要直接操作 DOM 时才使用。过度使用模板引用会使代码难以维护,并且可能破坏 Vue 的响应式系统。

2. 基本使用

基本模板引用示例
基础

<template>
  <div>
    <!-- 添加ref属性到DOM元素 -->
    <input ref="usernameInput"
           type="text"
           placeholder="请输入用户名">

    <button @click="focusInput">聚焦输入框</button>
    <button @click="showInputValue">显示输入值</button>

    <!-- 添加ref属性到子组件 -->
    <ChildComponent ref="childComponent" />

    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>
                                        

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  mounted() {
    // 组件挂载后,refs就可以访问了
    console.log('DOM元素引用:', this.$refs.usernameInput);
    console.log('子组件引用:', this.$refs.childComponent);

    // 可以立即聚焦输入框
    // this.$refs.usernameInput.focus();
  },
  methods: {
    focusInput() {
      // 访问DOM元素并调用其方法
      if (this.$refs.usernameInput) {
        this.$refs.usernameInput.focus();
      }
    },
    showInputValue() {
      // 访问DOM元素的value属性
      if (this.$refs.usernameInput) {
        const value = this.$refs.usernameInput.value;
        alert(`输入的值是: ${value}`);
      }
    },
    callChildMethod() {
      // 访问子组件实例并调用其方法
      if (this.$refs.childComponent) {
        this.$refs.childComponent.sayHello();
      }
    }
  }
};
</script>
                                        
模板引用演示

关键点:

  • 通过 ref 属性为元素或组件添加引用
  • 通过 this.$refs.refName 访问引用
  • 引用在组件挂载后可用
  • 可以访问DOM元素的方法和属性
  • 可以访问子组件实例的方法和数据

$refs 对象

$refs 是一个对象,包含所有注册了 ref 属性的元素或组件的引用:

$refs的结构

// $refs 对象示例
{
  usernameInput: HTMLInputElement, // DOM元素
  childComponent: VueComponent,     // 组件实例
  buttonElement: HTMLButtonElement, // 另一个DOM元素
  formElement: HTMLFormElement      // 表单元素
}

// 访问方式
console.log(this.$refs.usernameInput); // 输入框DOM元素
console.log(this.$refs.childComponent); // 子组件实例
console.log(this.$refs.buttonElement); // 按钮DOM元素
                                        
重要注意事项
  • $refs 只在组件渲染完成后才填充
  • $refs 不是响应式的,不应在模板中依赖它
  • $refs 只会在组件渲染后生效,并且不是响应式的
  • 避免在模板或计算属性中使用 $refs
  • mounted() 生命周期钩子中访问最安全
  • 如果使用 v-ifv-for,引用可能不存在

3. 访问DOM元素

最常见的模板引用用法是访问和操作DOM元素。以下是一些常见场景:

DOM操作示例
DOM操作

<template>
  <div>
    <h3>DOM元素操作示例</h3>

    <!-- 输入框焦点管理 -->
    <div class="mb-3">
      <label>用户名:</label>
      <input ref="username" type="text" class="form-control">
      <button @click="focusUsername" class="btn btn-sm btn-primary mt-2">
        聚焦用户名输入框
      </button>
    </div>

    <!-- 获取元素尺寸 -->
    <div ref="sizeElement" class="p-3 border">
      这个元素的尺寸是: {{ elementSize }}
    </div>
    <button @click="measureElement" class="btn btn-sm btn-info mt-2">
      测量元素尺寸
    </button>

    <!-- 滚动到元素 -->
    <div ref="targetElement" class="p-3 bg-warning mt-3">
      滚动到这里
    </div>
    <button @click="scrollToElement" class="btn btn-sm btn-warning mt-2">
      滚动到目标元素
    </button>

    <!-- 修改元素样式 -->
    <div ref="styleElement" class="p-3 mt-3">
      点击按钮修改我的样式
    </div>
    <button @click="changeStyle" class="btn btn-sm btn-success mt-2">
      修改样式
    </button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      elementSize: '未测量'
    };
  },
  methods: {
    focusUsername() {
      if (this.$refs.username) {
        this.$refs.username.focus();
        this.$refs.username.select(); // 选中文本
      }
    },
    measureElement() {
      if (this.$refs.sizeElement) {
        const rect = this.$refs.sizeElement.getBoundingClientRect();
        this.elementSize = `${Math.round(rect.width)}px × ${Math.round(rect.height)}px`;
      }
    },
    scrollToElement() {
      if (this.$refs.targetElement) {
        this.$refs.targetElement.scrollIntoView({
          behavior: 'smooth',
          block: 'center'
        });
      }
    },
    changeStyle() {
      if (this.$refs.styleElement) {
        const element = this.$refs.styleElement;
        element.style.backgroundColor = '#e74c3c';
        element.style.color = 'white';
        element.style.fontWeight = 'bold';
        element.style.borderRadius = '10px';
        element.textContent = '样式已修改!';
      }
    }
  }
};
</script>
                            
DOM操作演示
这个元素的尺寸是: 未测量
点击按钮修改我的样式

常见DOM操作方法

方法 描述 示例
.focus() 使元素获得焦点 this.$refs.input.focus()
.blur() 使元素失去焦点 this.$refs.input.blur()
.select() 选中元素的文本内容 this.$refs.input.select()
.click() 模拟元素点击 this.$refs.button.click()
.scrollIntoView() 滚动到元素可见 this.$refs.element.scrollIntoView()
.getBoundingClientRect() 获取元素位置和尺寸 this.$refs.element.getBoundingClientRect()
.querySelector() 在元素内查找子元素 this.$refs.container.querySelector('.item')
最佳实践: 虽然可以直接通过 $refs 修改 DOM 元素的样式,但在大多数情况下,更好的做法是通过数据绑定来修改 class 或 style。只有当数据绑定无法满足需求时,才直接操作 DOM。

4. 访问组件实例

除了访问 DOM 元素,ref 属性也可以用于访问子组件实例,从而调用子组件的方法或访问其数据。

组件实例访问示例
组件访问

<template>
  <div>
    <h3>父组件</h3>

    <!-- 引用子组件 -->
    <Counter ref="counterComponent" />

    <div class="mt-3">
      <button @click="incrementCounter" class="btn btn-primary">
        调用子组件的 increment 方法
      </button>

      <button @click="resetCounter" class="btn btn-warning">
        调用子组件的 reset 方法
      </button>

      <button @click="getCounterValue" class="btn btn-info">
        获取子组件的 count 值
      </button>

      <button @click="callCustomMethod" class="btn btn-success">
        调用子组件的自定义方法
      </button>
    </div>

    <div class="mt-3">
      <p>从子组件获取的值: {{ childValue }}</p>
    </div>
  </div>
</template>

<script>
import Counter from './Counter.vue';

export default {
  components: { Counter },
  data() {
    return {
      childValue: null
    };
  },
  methods: {
    incrementCounter() {
      if (this.$refs.counterComponent) {
        this.$refs.counterComponent.increment();
      }
    },
    resetCounter() {
      if (this.$refs.counterComponent) {
        this.$refs.counterComponent.reset();
      }
    },
    getCounterValue() {
      if (this.$refs.counterComponent) {
        // 访问子组件的数据
        this.childValue = this.$refs.counterComponent.count;
        alert(`子组件的count值为: ${this.childValue}`);
      }
    },
    callCustomMethod() {
      if (this.$refs.counterComponent) {
        // 调用子组件的方法
        const result = this.$refs.counterComponent.formatCount();
        alert(`格式化后的count值: ${result}`);
      }
    }
  }
};
</script>
                                        

<template>
  <div class="counter">
    <h4>计数器组件</h4>
    <p>当前计数: {{ count }}</p>

    <div>
      <button @click="increment" class="btn btn-sm btn-primary">增加</button>
      <button @click="decrement" class="btn btn-sm btn-danger">减少</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Counter',
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
      console.log('计数器增加:', this.count);
    },
    decrement() {
      this.count--;
      console.log('计数器减少:', this.count);
    },
    reset() {
      this.count = 0;
      console.log('计数器重置');
    },
    // 自定义方法,可以被父组件调用
    formatCount() {
      return `计数: ${this.count}`;
    },
    // 另一个自定义方法
    multiplyBy(factor) {
      this.count *= factor;
      return this.count;
    }
  }
};
</script>

<style scoped>
.counter {
  padding: 1rem;
  border: 2px solid #42b983;
  border-radius: 0.5rem;
  background-color: #f8f9fa;
}
</style>
                                        
计数器组件 (子组件)

当前计数: 0

父组件控制区

从子组件获取的值: null

可以访问的组件属性和方法

访问内容 描述 示例
数据属性 访问子组件的响应式数据 this.$refs.child.someData
计算属性 访问子组件的计算属性 this.$refs.child.someComputed
方法 调用子组件的方法 this.$refs.child.someMethod()
生命周期钩子 某些钩子可以被外部调用(不推荐) this.$refs.child.$forceUpdate()
DOM元素 访问子组件的根DOM元素 this.$refs.child.$el
子组件 访问子组件的子组件引用 this.$refs.child.$refs.grandChild
注意: 直接访问和修改子组件的数据破坏了组件的封装性,应该谨慎使用。通常,更好的做法是通过props传递数据,通过事件进行通信。只有在确实需要时才使用ref来访问子组件。

5. v-for中的引用

当在 v-for 中使用 ref 时,得到的引用将是一个包含对应DOM元素或组件实例的数组。

v-for中的引用示例
v-for

<template>
  <div>
    <h3>v-for中的模板引用</h3>

    <!-- 添加项目 -->
    <div class="mb-3">
      <input v-model="newItem" type="text" placeholder="输入新项目">
      <button @click="addItem" class="btn btn-sm btn-primary">添加项目</button>
    </div>

    <!-- 项目列表 -->
    <ul>
      <li v-for="(item, index) in items"
          :key="index"
          ref="itemElements">
        {{ item }}
        <button @click="highlightItem(index)" class="btn btn-sm btn-info">
          高亮此项
        </button>
      </li>
    </ul>

    <!-- 操作按钮 -->
    <div class="mt-3">
      <button @click="highlightAll" class="btn btn-warning">
        高亮所有项目
      </button>
      <button @click="clearHighlights" class="btn btn-secondary">
        清除高亮
      </button>
      <button @click="getFirstItemText" class="btn btn-info">
        获取第一个项目文本
      </button>
    </div>

    <!-- 使用v-for引用组件 -->
    <h4 class="mt-4">组件列表</h4>
    <div v-for="(data, index) in componentData"
         :key="'comp-' + index">
      <ChildComponent
        :value="data"
        ref="childComponents">
      </ChildComponent>
    </div>
    <button @click="callAllComponents" class="btn btn-success mt-2">
      调用所有组件方法
    </button>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent },
  data() {
    return {
      items: ['项目A', '项目B', '项目C', '项目D'],
      newItem: '',
      componentData: [10, 20, 30]
    };
  },
  methods: {
    addItem() {
      if (this.newItem.trim()) {
        this.items.push(this.newItem);
        this.newItem = '';

        // 注意:当添加新项目后,$refs.itemElements会自动更新
        // 但需要在下次DOM更新后才能访问新添加的引用
        this.$nextTick(() => {
          console.log('当前项目元素数量:', this.$refs.itemElements.length);
        });
      }
    },
    highlightItem(index) {
      // $refs.itemElements是一个数组
      if (this.$refs.itemElements && this.$refs.itemElements[index]) {
        const element = this.$refs.itemElements[index];
        element.style.backgroundColor = '#ffeb3b';
        element.style.fontWeight = 'bold';
      }
    },
    highlightAll() {
      // 遍历所有引用
      if (this.$refs.itemElements) {
        this.$refs.itemElements.forEach(element => {
          element.style.backgroundColor = '#ffeb3b';
          element.style.fontWeight = 'bold';
        });
      }
    },
    clearHighlights() {
      if (this.$refs.itemElements) {
        this.$refs.itemElements.forEach(element => {
          element.style.backgroundColor = '';
          element.style.fontWeight = '';
        });
      }
    },
    getFirstItemText() {
      if (this.$refs.itemElements && this.$refs.itemElements[0]) {
        const text = this.$refs.itemElements[0].textContent;
        alert(`第一个项目的文本: ${text}`);
      }
    },
    callAllComponents() {
      // $refs.childComponents是一个组件实例数组
      if (this.$refs.childComponents) {
        this.$refs.childComponents.forEach((component, index) => {
          console.log(`调用组件${index}的方法`);
          // 假设组件有一个doSomething方法
          // component.doSomething();
        });
        alert(`调用了 ${this.$refs.childComponents.length} 个组件的方法`);
      }
    }
  },
  mounted() {
    // 在mounted中访问引用数组
    console.log('项目元素引用数组:', this.$refs.itemElements);
    console.log('组件引用数组:', this.$refs.childComponents);
  }
};
</script>
                            
v-for引用演示
  • 项目A
  • 项目B
  • 项目C
  • 项目D

v-for引用的重要特性

特性 描述 注意事项
数组形式 v-for 中的 ref 会生成一个数组 不是单个引用,需要按索引访问
自动更新 当列表变化时,引用数组会自动更新 确保在 $nextTick 后访问新引用
顺序一致 引用数组的顺序与数据数组顺序一致 使用 :key 确保稳定
组件引用 也可以引用组件实例数组 可以批量调用组件方法
性能考虑 大型列表中使用引用可能影响性能 避免在大型列表中使用
使用 $nextTick 当通过 v-for 渲染的列表动态变化时(添加、删除、重新排序),Vue 需要更新 DOM。为了确保能访问到最新的引用,应该在 $nextTick 回调中访问 $refs

6. 函数形式的引用

除了字符串形式的引用,Vue 还支持函数形式的引用。这为你提供了更大的灵活性,可以在元素挂载和卸载时执行自定义逻辑。

函数形式引用示例
函数形式

<template>
  <div>
    <h3>函数形式的模板引用</h3>

    <!-- 函数形式ref -->
    <input :ref="(el) => setInputRef(el)"
           type="text"
           placeholder="函数形式ref示例">

    <button @click="focusDynamicRef" class="btn btn-primary">
      聚焦输入框
    </button>

    <!-- 动态绑定ref函数 -->
    <div class="mt-3">
      <label>选择要引用的元素:</label>
      <select v-model="selectedElement">
        <option value="first">第一个输入框</option>
        <option value="second">第二个输入框</option>
      </select>
    </div>

    <!-- 多个元素,使用动态ref -->
    <input v-if="selectedElement === 'first'"
           :ref="dynamicRef"
           type="text"
           placeholder="第一个输入框">

    <input v-else
           :ref="dynamicRef"
           type="text"
           placeholder="第二个输入框">

    <!-- 使用ref清理函数 -->
    <div class="mt-4">
      <button @click="toggleSpecialElement" class="btn btn-warning">
        {{ showSpecialElement ? '隐藏' : '显示' }}特殊元素
      </button>

      <div v-if="showSpecialElement"
           :ref="specialElementRef"
           class="p-3 bg-info text-white mt-2">
        这是一个特殊元素
      </div>
    </div>

    <!-- 显示引用信息 -->
    <div class="mt-3">
      <p>当前引用的元素: {{ currentRefType }}</p>
      <p>特殊元素引用: {{ specialElementInfo }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      inputRef: null,
      dynamicElementRef: null,
      selectedElement: 'first',
      showSpecialElement: false,
      specialElementRefValue: null,
      currentRefType: '无',
      specialElementInfo: '无'
    };
  },
  computed: {
    // 计算属性返回ref函数
    dynamicRef() {
      return (el) => {
        this.dynamicElementRef = el;
        this.currentRefType = this.selectedElement === 'first'
          ? '第一个输入框'
          : '第二个输入框';
        console.log('动态ref函数被调用,元素:', el);
      };
    },
    // 带有清理逻辑的ref函数
    specialElementRef() {
      return (el) => {
        if (el) {
          // 元素被挂载
          this.specialElementRefValue = el;
          this.specialElementInfo = `已挂载,标签名: ${el.tagName}`;
          console.log('特殊元素已挂载:', el);

          // 可以在这里执行一些初始化操作
          el.style.border = '2px solid red';
        } else {
          // 元素被卸载 (el为null)
          this.specialElementInfo = '已卸载';
          console.log('特殊元素已卸载');

          // 可以在这里执行一些清理操作
          if (this.specialElementRefValue) {
            // 清理操作
            this.specialElementRefValue = null;
          }
        }
      };
    }
  },
  methods: {
    setInputRef(el) {
      this.inputRef = el;
      console.log('输入框引用已设置:', el);

      // 可以立即执行一些操作
      if (el) {
        el.style.borderColor = '#42b983';
      }
    },
    focusDynamicRef() {
      if (this.inputRef) {
        this.inputRef.focus();
        this.inputRef.select();
      }
    },
    toggleSpecialElement() {
      this.showSpecialElement = !this.showSpecialElement;
    }
  }
};
</script>
                            
函数形式引用演示

当前引用的元素: 第一个输入框

特殊元素引用:

函数形式引用的优势

特性 函数形式ref 字符串形式ref
灵活性 可以在挂载/卸载时执行自定义逻辑 只能自动注册到$refs对象
动态性 可以根据条件动态设置引用 静态的,不能在运行时改变
清理逻辑 可以在元素卸载时执行清理 无清理机制
多个引用 可以使用同一个函数处理多个元素 每个元素需要独立的ref名称
控制权 完全控制引用存储方式 只能存储在$refs对象中
何时使用函数形式ref: 当你需要在元素挂载或卸载时执行特定逻辑时,或者当你需要动态控制引用时,函数形式的ref非常有用。例如,集成第三方库时,你可能需要在元素挂载时初始化库,在元素卸载时清理资源。

7. 组合式API中的引用

在Vue 3的组合式API中,模板引用通过 ref 函数创建。这与响应式系统的 ref 是同一个函数,但用法略有不同。

组合式API中的模板引用
组合式API

<template>
  <div>
    <!-- 模板中使用ref属性绑定到响应式引用 -->
    <input ref="inputRef" type="text" placeholder="请输入内容">

    <button @click="handleClick">聚焦输入框</button>

    <!-- 引用子组件 -->
    <ChildComponent ref="childRef" />

    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';

// 创建模板引用
const inputRef = ref(null); // 初始值为null
const childRef = ref(null); // 子组件引用

// 在setup中访问引用
console.log('初始inputRef:', inputRef.value); // null

onMounted(() => {
  // 组件挂载后,引用会被自动填充
  console.log('挂载后inputRef:', inputRef.value); // DOM元素
  console.log('挂载后childRef:', childRef.value); // 组件实例

  // 可以立即聚焦输入框
  // inputRef.value?.focus();
});

function handleClick() {
  // 访问DOM元素
  if (inputRef.value) {
    inputRef.value.focus();
    inputRef.value.select();
  }
}

function callChildMethod() {
  // 访问子组件实例
  if (childRef.value) {
    childRef.value.someMethod();
  }
}
</script>
                                        

<template>
  <div>
    <!-- 动态绑定ref -->
    <input v-if="showFirst" ref="firstInput" type="text" placeholder="第一个输入框">
    <input v-else ref="secondInput" type="text" placeholder="第二个输入框">

    <button @click="toggleInput">切换输入框</button>
    <button @click="focusCurrentInput">聚焦当前输入框</button>

    <!-- 使用计算属性管理引用 -->
    <div ref="dynamicElementRef" class="p-3" :class="elementClass">
      动态元素
    </div>
    <button @click="changeElementClass">改变元素类</button>
  </div>
</template>

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

const showFirst = ref(true);
const firstInput = ref(null);
const secondInput = ref(null);
const dynamicElementRef = ref(null);

// 计算当前活跃的输入框引用
const currentInputRef = computed(() => {
  return showFirst.value ? firstInput.value : secondInput.value;
});

// 计算元素类
const elementClass = computed(() => {
  return dynamicElementRef.value ? 'active' : 'inactive';
});

// 监听引用变化
watch(currentInputRef, (newRef, oldRef) => {
  console.log('当前输入框引用变化:', { old: oldRef, new: newRef });

  if (newRef) {
    // 新输入框挂载后自动聚焦
    setTimeout(() => {
      newRef.focus();
    }, 100);
  }
});

// 监听动态元素引用变化
watch(dynamicElementRef, (newElement, oldElement) => {
  console.log('动态元素引用变化:', { old: oldElement, new: newElement });

  if (newElement) {
    newElement.style.border = '2px solid #42b983';
  }
});

function toggleInput() {
  showFirst.value = !showFirst.value;
}

function focusCurrentInput() {
  if (currentInputRef.value) {
    currentInputRef.value.focus();
  }
}

function changeElementClass() {
  if (dynamicElementRef.value) {
    const classes = ['primary', 'warning', 'success', 'danger'];
    const randomClass = classes[Math.floor(Math.random() * classes.length)];
    dynamicElementRef.value.className = `p-3 bg-${randomClass} text-white`;
  }
}
</script>

<style>
.active {
  background-color: #e8f4ff;
  border: 2px solid #3498db;
}
.inactive {
  background-color: #f8f9fa;
  border: 1px dashed #ccc;
}
</style>
                                        

<template>
  <div>
    <h3>监听模板引用变化</h3>

    <!-- 条件渲染的元素 -->
    <button @click="toggleElement">
      {{ showElement ? '隐藏' : '显示' }}元素
    </button>

    <div v-if="showElement" ref="conditionalRef" class="p-3 mt-3">
      条件渲染的元素
    </div>

    <!-- 显示引用状态 -->
    <p>引用状态: {{ refStatus }}</p>
    <p>引用变化次数: {{ changeCount }}</p>

    <!-- v-for中的引用监听 -->
    <div class="mt-4">
      <button @click="addItem">添加项目</button>
      <button @click="removeItem">移除项目</button>

      <ul>
        <li v-for="item in items"
            :key="item.id"
            :ref="(el) => setItemRef(item.id, el)">
          {{ item.name }}
        </li>
      </ul>

      <p>项目引用数量: {{ itemRefs.size }}</p>
    </div>
  </div>
</template>

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

const showElement = ref(false);
const conditionalRef = ref(null);
const refStatus = ref('无引用');
const changeCount = ref(0);

const items = ref([
  { id: 1, name: '项目1' },
  { id: 2, name: '项目2' },
  { id: 3, name: '项目3' }
]);

const itemRefs = ref(new Map()); // 使用Map存储多个引用

// 监听条件引用变化
watch(conditionalRef, (newElement, oldElement) => {
  changeCount.value++;

  if (newElement) {
    refStatus.value = `元素已挂载,标签名: ${newElement.tagName}`;
    console.log('元素挂载:', newElement);

    // 执行一些初始化操作
    newElement.style.backgroundColor = '#e8f4ff';
    newElement.style.transition = 'background-color 0.3s';
  } else if (oldElement) {
    refStatus.value = '元素已卸载';
    console.log('元素卸载:', oldElement);
  }
}, {
  // 立即触发一次回调,初始值为null
  immediate: true
});

// 项目引用设置函数
function setItemRef(id, element) {
  if (element) {
    // 元素挂载
    itemRefs.value.set(id, element);
    console.log(`项目${id}引用已设置:`, element);
  } else {
    // 元素卸载
    itemRefs.value.delete(id);
    console.log(`项目${id}引用已移除`);
  }
}

// 监听项目引用Map的变化
watch(() => itemRefs.value.size, (newSize, oldSize) => {
  console.log(`项目引用数量变化: ${oldSize} -> ${newSize}`);
});

function toggleElement() {
  showElement.value = !showElement.value;
}

function addItem() {
  const newId = items.value.length + 1;
  items.value.push({ id: newId, name: `项目${newId}` });
}

function removeItem() {
  if (items.value.length > 0) {
    items.value.pop();
  }
}

// 组件卸载时清理
onUnmounted(() => {
  console.log('组件卸载,清理所有引用');
  itemRefs.value.clear();
});
</script>
                                        

组合式API vs 选项式API

特性 组合式API 选项式API
创建引用 const refName = ref(null) 直接在模板中使用 ref="refName"
访问引用 refName.value this.$refs.refName
类型支持 更好的TypeScript支持 类型支持有限
响应性 引用本身是响应式的 $refs 不是响应式的
动态引用 更容易创建动态引用 需要使用函数形式ref
监听变化 可以使用 watch 监听 无法直接监听
组合式API的优势: 在组合式API中,模板引用是真正的响应式引用,可以使用 watch 监听其变化,可以更好地与TypeScript集成,并且可以更灵活地组织代码逻辑。

8. 使用场景

表单焦点管理

场景: 自动聚焦输入框或在表单验证失败时聚焦错误字段


// 自动聚焦第一个输入框
mounted() {
  this.$refs.firstInput.focus();
}

// 表单验证失败后聚焦错误字段
methods: {
  validateForm() {
    if (!this.$refs.emailInput.value) {
      this.$refs.emailInput.focus();
      return false;
    }
    return true;
  }
}
                                        
集成第三方库

场景: 集成图表库、地图库或其他需要DOM操作的库


// 集成图表库
mounted() {
  this.chart = new Chart(this.$refs.chartCanvas, {
    type: 'bar',
    data: chartData,
    options: chartOptions
  });
},
beforeDestroy() {
  // 清理图表
  if (this.chart) {
    this.chart.destroy();
  }
}
                                        
测量元素尺寸

场景: 获取元素尺寸用于布局计算或响应式设计


// 获取元素尺寸
methods: {
  measureElement() {
    const rect = this.$refs.targetElement.getBoundingClientRect();
    return {
      width: rect.width,
      height: rect.height,
      top: rect.top,
      left: rect.left
    };
  }
},
mounted() {
  // 监听窗口大小变化
  window.addEventListener('resize', () => {
    const size = this.measureElement();
    this.updateLayout(size);
  });
}
                                        
调用子组件方法

场景: 父组件需要直接调用子组件的方法


// 父组件
methods: {
  saveForm() {
    // 调用子组件的验证方法
    if (this.$refs.formComponent.validate()) {
      this.$refs.formComponent.submit();
    }
  },
  resetForm() {
    this.$refs.formComponent.reset();
  }
}

// 子组件
methods: {
  validate() {
    // 验证逻辑
    return this.isValid;
  },
  submit() {
    // 提交逻辑
  },
  reset() {
    // 重置逻辑
  }
}
                                        
滚动操作

场景: 实现平滑滚动、滚动到特定元素或无限滚动


// 滚动到特定元素
methods: {
  scrollToElement(elementId) {
    const element = this.$refs[elementId];
    if (element) {
      element.scrollIntoView({
        behavior: 'smooth',
        block: 'start'
      });
    }
  },

  // 无限滚动
  handleScroll() {
    const container = this.$refs.scrollContainer;
    if (container) {
      const scrollBottom = container.scrollHeight -
                          container.scrollTop -
                          container.clientHeight;

      if (scrollBottom < 100) { // 接近底部
        this.loadMoreItems();
      }
    }
  }
},
mounted() {
  // 添加滚动监听
  const container = this.$refs.scrollContainer;
  if (container) {
    container.addEventListener('scroll', this.handleScroll);
  }
}
                                        

9. 最佳实践

应该使用模板引用的场景
  • 焦点管理:输入框自动聚焦
  • 文本选择:选中输入框中的文本
  • 集成第三方库:需要DOM元素的库
  • 测量元素:获取元素尺寸和位置
  • 媒体播放:控制视频或音频播放
  • 动画控制:直接操作DOM动画
  • 调用子组件方法:当事件系统不够用时
应该避免使用模板引用的场景
  • 数据绑定:应该使用v-model
  • 样式修改:应该使用class或style绑定
  • 条件渲染:应该使用v-if或v-show
  • 列表渲染:应该使用v-for
  • 表单验证:应该使用计算属性或watcher
  • 组件通信:应该使用props和events
  • 状态管理:应该使用Vuex/Pinia

最佳实践总结

实践 说明 示例
使用前检查 总是检查引用是否存在 if (this.$refs.input) { ... }
生命周期时机 在mounted后访问引用 mounted()$nextTick()中访问
避免模板中使用 不要在模板中直接使用$refs 使用数据绑定而不是{{ $refs.input.value }}
使用函数形式ref 需要清理逻辑时使用函数形式 :ref="(el) => { ... }"
类型安全 在TypeScript中为引用添加类型 const inputRef = ref<HTMLInputElement>(null)
组件封装 优先通过props/events通信 避免过度使用ref访问子组件
性能考虑 避免在大型v-for中使用ref 每个ref都会创建额外的引用
重要警告: 模板引用是直接操作DOM的"逃生舱口",应该谨慎使用。过度使用模板引用会使代码难以维护,破坏Vue的声明式特性,并可能导致难以调试的问题。优先考虑使用Vue的声明式特性(数据绑定、计算属性、侦听器等)来解决问题。

总结

模板引用(Template Refs)是Vue.js中强大的特性,允许直接访问DOM元素和组件实例。关键要点:

  • 基本使用: 通过 ref 属性创建引用,通过 $refs 对象或响应式 ref 访问
  • DOM操作: 可以直接访问和操作DOM元素的方法和属性
  • 组件访问: 可以访问子组件实例,调用其方法和访问其数据
  • v-for引用: 在循环中创建引用数组,可以批量操作元素
  • 函数形式: 提供更大的灵活性,可以在挂载/卸载时执行自定义逻辑
  • 组合式API: 提供更好的类型支持和响应式引用

正确使用模板引用可以解决一些特殊场景下的问题,但应该避免过度使用。在大多数情况下,优先使用Vue的声明式特性,只有在确实需要直接操作DOM时才使用模板引用。