Code前端首页关于Code前端联系我们

Vue2里的ref到底怎么用?新手也能看懂的实用指南

terry 7小时前 阅读数 10 #Vue
文章标签 Vue2;ref

刚接触Vue2的小伙伴,是不是一碰到ref就有点懵?明明看文档知道它能“抓”DOM、调组件方法,可实际写代码时不是拿不到值,就是分不清和data的区别……别慌,这篇文章把ref从基础到实战的知识点拆碎了讲,不管是操作DOM、组件通信还是处理列表场景,看完你就能上手用!

ref到底是什么?和Vue里其他响应式数据有啥不同?

先把概念掰碎了说:ref是Vue2里的一个特殊属性,你给DOM元素或者组件标签加上ref属性后,Vue会把对应的DOM元素或者组件实例,“存”到当前Vue实例的$refs对象里,打个比方,ref就像给DOM/组件贴了个“身份证”,$refs就是个“储物柜”,凭这个身份证能随时把对应的东西取出来用。

那它和Vue里的data有啥不一样?最核心的区别是响应式,咱都知道data里的变量是响应式的——数据变了,页面自动跟着变;页面上用户操作变了,数据也能同步,但ref和$refs是非响应式的!也就是说,你直接修改$refs里存的东西,不会触发页面更新;反过来,页面结构变了(比如组件被销毁),$refs里的引用会跟着变,但这个变化也不会触发Vue的响应式更新逻辑。

举个简单例子:你给一个输入框加ref="myInput",然后在方法里写this.$refs.myInput.focus(),就能直接让输入框获得焦点——这就是用ref“抓”DOM,调用DOM原生方法,要是换成data,你没法直接用data里的变量去调DOM方法,因为data存的是数据,不是DOM本身。

给DOM元素加ref,能玩出哪些花样?

给普通DOM元素加ref,本质是“拿到真实DOM的控制权”,日常开发里,这些场景特别实用:

页面加载后自动聚焦

比如做登录页,希望用户打开页面后,光标直接定位在用户名输入框,用ref就能轻松实现:

<template>  
  <input ref="usernameInput" type="text" placeholder="请输入用户名" />  
</template>  
<script>  
export default {  
  mounted() {  
    // mounted阶段DOM已经渲染完成,能拿到ref  
    this.$refs.usernameInput.focus();   
  }  
}  
</script>  

这里要注意生命周期时机created阶段DOM还没生成,这时候访问$refs会拿到undefined,所以得在mounted或者更晚的生命周期里操作。

动态修改DOM样式/属性

有时候产品经理想要“点击按钮换背景色”这种交互,用ref直接操作DOM比写一堆响应式数据更简单。

<template>  
  <div ref="box" class="box">我是个盒子</div>  
  <button @click="changeColor">换色</button>  
</template>  
<script>  
export default {  
  methods: {  
    changeColor() {  
      this.$refs.box.style.backgroundColor = 'pink';  
    }  
  }  
}  
</script>  
<style>  
.box { width: 100px; height: 100px; background: lightblue; }  
</style>  

不过要提醒一句:Vue鼓励“数据驱动视图”,如果是简单的样式切换,优先用:class:style绑定数据(比如用data里的isPink变量控制class),直接操作DOM适合那种“必须手动改DOM”的场景,比如第三方库要求传DOM元素才能初始化(像某些图表库)。

批量操作列表里的DOM(v-for场景)

如果ref用在v-for循环里,$refs的表现会不一样,分两种情况:

  • 静态ref(ref是固定字符串):比如

  • {{item}}
  • ,这时候this.$refs.listItem一个数组,数组里的每个元素对应循环中每个li的DOM,你可以遍历这个数组,批量操作所有列表项。

  • 动态ref(ref绑定表达式):比如

  • {{item}}
  • ,这时候this.$refs一个对象,键是item_0item_1…值是对应的DOM,这种适合“精准操作某一个列表项”的场景,比如点击某个列表项的编辑按钮,只修改它的样式。

举个实际例子: todo列表里每个项有“完成”按钮,点击后给对应项加删除线,用动态ref的话,代码可以这样写:

<template>  
  <ul>  
    <li  
      v-for="(todo, index) in todoList"   
      :key="index"   
      :ref="`todo_${index}`"  
    >  
      {{todo.text}}   
      <button @click="markAsDone(index)">完成</button>  
    </li>  
  </ul>  
</template>  
<script>  
export default {  
  data() {  
    return {  
      todoList: [  
        { text: '买咖啡' },  
        { text: '写代码' },  
        { text: '遛狗' }  
      ]  
    }  
  },  
  methods: {  
    markAsDone(index) {  
      const todoDom = this.$refs[`todo_${index}`][0]; // 因为ref绑定到li,所以取数组第0个  
      todoDom.style.textDecoration = 'line-through';  
    }  
  }  
}  
</script>  

这里注意:v-for循环的DOM,ref对应的$refs值是数组(哪怕只有一个元素),所以要取[0]才能拿到真实DOM,这是因为Vue在处理v-for的ref时,会把每个循环项的DOM收集到数组里,哪怕你用动态ref,每个键对应的也是数组(因为理论上v-for可能渲染多个相同ref的元素?其实动态ref的键是唯一的,所以数组长度是1,但Vue统一处理成数组)。

给组件加ref,怎么实现组件间“隔空传功”?

除了DOM,ref还能“抓”组件——准确说,是父组件通过ref拿到子组件的实例,然后调用子组件的方法、访问子组件的data或props,这在父→子通信里特别有用(虽然Vue更推荐用props和$emit,但某些场景下ref更直接)。

调用子组件的方法

比如子组件是个弹窗组件,里面有个open()方法控制弹窗显示,父组件想点按钮打开弹窗,就可以用ref:

<!-- 父组件 Parent.vue -->  
<template>  
  <button @click="openChildDialog">打开弹窗</button>  
  <ChildDialog ref="childDialog" />  
</template>  
<script>  
import ChildDialog from './ChildDialog.vue'  
export default {  
  components: { ChildDialog },  
  methods: {  
    openChildDialog() {  
      this.$refs.childDialog.open(); // 调用子组件的open方法  
    }  
  }  
}  
</script>  
<!-- 子组件 ChildDialog.vue -->  
<template><div v-show="isShow">我是弹窗内容</div></template>  
<script>  
export default {  
  data() { return { isShow: false } },  
  methods: {  
    open() {  
      this.isShow = true;  
    }  
  }  
}  
</script>  

这里要注意组件的渲染时机:如果子组件是用v-if控制显示(比如),那当showDialog为false时,子组件没被渲染,this.$refs.childDialog会是undefined,所以要确保调用ref方法时,组件已经被渲染,可以用$nextTick或者把v-if改成v-show(v-show是隐藏,组件还在)。

访问子组件的data或props

理论上,父组件通过ref拿到子组件实例后,可以直接访问子组件的data里的变量,甚至修改props(虽然不推荐改props,因为props是父传子的,单向数据流),比如子组件有个props叫title,父组件想拿到它:

<!-- 子组件 Child.vue -->  
<script>  
export default {  
  props: { title: String },  
  data() { return { count: 0 } }  
}  
</script>  
<!-- 父组件 Parent.vue -->  
<template><Child ref="childComp" :title="parentTitle" /></template>  
<script>  
export default {  
  data() { return { parentTitle: '子组件标题' } },  
  mounted() {  
    console.log(this.$refs.childComp.title); // 拿到子组件的props.title  
    console.log(this.$refs.childComp.count); // 拿到子组件的data.count  
    this.$refs.childComp.count = 1; // 直接修改子组件的data(不推荐,容易乱)  
  }  
}  
</script>  

不建议直接改子组件的data!因为Vue的响应式是基于数据驱动,父组件直接改子组件内部状态,会让代码逻辑变混乱,出了bug很难查,如果要改子组件状态,优先让子组件自己暴露方法(比如子组件写个updateCount方法,父组件调这个方法),或者用$emit通知父组件,父组件再通过props传值下去。

什么时候适合用ref调组件?

ref调组件适合“父组件需要主动触发子组件的某个操作,且这个操作是纯‘行为’(比如打开弹窗、触发动画、调用第三方库方法)”的场景,如果是数据相关的交互,还是用props和$emit更规范,避免组件间耦合太严重。

ref在v-for循环里,怎么精准“抓”到目标?

前面讲DOM时提了v-for里的ref,这里专门讲组件在v-for里的ref,因为场景更复杂,也更容易踩坑。

循环组件时,$refs的表现

和DOM一样,组件在v-for里用ref,$refs的规则也分静态和动态:

  • 静态ref(比如ref="childComp"):this.$refs.childComp数组,数组里每个元素是循环中每个子组件的实例。
  • 动态ref(ref="child_${index}"):this.$refs对象,键是child_0、child_1…值是对应的子组件实例。

举个场景:做一个tabs组件,每个tab是子组件,父组件要批量获取所有tab的高度,用来做滚动计算,用静态ref+数组遍历:

<template>  
  <div class="tabs">  
    <Tab   
      v-for="(tab, index) in tabList"   
      :key="index"   
      ref="tabComp"   
      :title="tab.title"  
    />  
  </div>  
</template>  
<script>  
import Tab from './Tab.vue'  
export default {  
  components: { Tab },  
  data() {  
    return {  
      tabList: [  
        { title: '首页' },  
        { title: '分类' },  
        { title: '我的' }  
      ]  
    }  
  },  
  mounted() {  
    // 遍历所有tab组件实例,拿到高度  
    this.$refs.tabComp.forEach(tab => {  
      console.log(tab.$el.offsetHeight); // tab组件的根元素高度  
    });  
  }  
}  
</script>  

这里用到了子组件的$el——每个Vue组件实例都有$el属性,对应组件的根DOM元素,所以通过ref拿到子组件实例后,可以访问$el来操作DOM。

循环里ref的“坑”:异步渲染和动态增减

如果v-for的数据源是异步获取的(比如从接口拿数据),那mounted里直接访问$refs可能拿不全,因为数据还没渲染完,这时候要把操作放到$nextTick里,等DOM更新后再执行:

<script>  
export default {  
  data() { return { tabList: [] } },  
  mounted() {  
    // 模拟异步请求  
    setTimeout(() => {  
      this.tabList = [/* 接口返回的数据 */];  
      this.$nextTick(() => {  
        // 现在tabList渲染完成,$refs.tabComp有值了  
        console.log(this.$refs.tabComp);   
      });  
    }, 1000);  
  }  
}  
</script>  

如果列表项是动态增减的(比如用户可以添加/删除tab),$refs数组的长度会跟着变,但Vue不会主动通知你这个变化(因为$refs非响应式),所以如果在方法里依赖$refs的长度,要注意及时更新逻辑。

ref和$refs的“雷区”,这些坑别踩!

用ref时踩过的坑,总结成这几个关键点,避开就能少掉头发:

生命周期里的“时间差”:created拿不到ref

created阶段Vue刚初始化完数据,DOM还没开始渲染,所以这时候访问$refs肯定是undefined,必须等到mounted(DOM渲染完成)或者用$nextTick(DOM更新后)才能拿到ref。

反例:

```html export default { created() { this.$refs.myInput.focus(); // 报错!因为created时DOM没渲染,$refs.myInput是undefined } } ```

正例:

```html export default { mounted() { this.$refs.myInput.focus(); // 正确,mounted时DOM已渲染 } } ```

别把$refs当响应式数据用

因为$refs本身不是响应式对象,

  • 不要把$refs里的值放到计算属性(computed)里,因为计算属性依赖响应式数据,$refs变化不会触发计算属性更新;
  • 不要在watch里监听$refs的变化,同样监听不到;
  • 也不要把$refs里的DOM或组件实例存到data里,因为data是响应式的,存非响应式的东西会导致更新异常。

反例:

```html export default { data() { return { inputDom: null } }, mounted() { this.inputDom = this.$refs.myInput; // 把非响应式的DOM存到data里,危险! }, computed: { inputValue() { return this.inputDom.value; // 一旦inputDom变化,computed不会更新 } } } ```

组件销毁后,$refs会“消失”

如果子组件是用v-if控制显示(比如),当show为false时,子组件会被销毁,这时候this.$refs.child就变成undefined了,所以在调用ref之前,最好判断一下是否存在:

if (this.$refs.child) {  
  this.$refs.child.doSomething();  
}  

别过度依赖ref操作DOM

Vue的核心思想是“数据驱动视图”,能通过数据绑定(:class/:style/v-show等)实现的交互,就别用ref手动改DOM,比如切换样式,用data里的isActive控制class,比this.$refs.box.className = 'active'更优雅,也更易维护。

ref只适合必须直接操作DOM的场景:比如调用DOM原生方法(focus、scrollIntoView)、给第三方库传DOM元素(比如ECharts需要把图表渲染到某个div里,这时候用ref拿div的DOM给ECharts初始化)。

实战案例:用ref搞定三个真实场景

光讲理论不够,结合实际场景练手,才能真正掌握ref的用法,下面三个案例,覆盖DOM操作、组件通信、列表交互,跟着做一遍就懂了!

案例1:表单自动聚焦+实时验证

需求:页面加载后

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门