Vue3中ref模板引用的理解与实践

6,309 阅读2分钟

认识ref引用

3句话简单介绍ref是个什么鬼

  1. 地球人都知道,Vue.js模板当中,既可以写Vue组件,也可以写具体的HTML标签。

  2. 同时,Vue提供了一个特殊的ref属性来对普通元素或组件建立引用。

    <template>
        <div class="page">
            <div class="img-container">
                <!-- 普通HTML -->
                <ul ref="imgListElm">
                    <li><img src=""></li>
                </ul>
            </div>
            <!-- Vue组件 -->
            <van-button type="primary" ref="imgBtn">主要按钮</van-button>
        </div>
    </template>
    
  3. 如果ref定义在HTML标签,那么引用的就是渲染后具体的DOM对象;如果定义在Vue组件上,则引用组件的Vue实例,进而获取Vue实例内的data数据,或者调用methods方法。

Vue2.x与Vue3.x使用ref的区别

在视图定义了ref引用后,Vue在编译期间就会自动建立绑定关系,把ref引用挂载到Vue组件实例中,我们在<script>代码中就能直接使用了,但Vue2与Vue3的使用是有些许区别的:

  • Vue2.x将定义的所有ref引用,自动归集在this.$refs集合中。
export default {
    mounted(){
        // 普通DOM对象
        let $imgListElm = this.$refs.imgListElm;
        console.log($imgListElm.children)

        // Vue组件对象
        let imgBtn = this.$refs.imgBtn;
        imgBtn.primary = 'warning'
    }
}
  • Vue3.x需要我们手动把ref引用,赋值到变量。
// composition API风格
// 提前要把用到的接口,都先import进来
import { ref, onMounted } from 'vue'

export default {
    setup(){

        // 使用ref()响应式API,定义同名的变量接收ref引用
        const $imgListElm = ref(null)
        const imgBtn = ref(null)

        onMounted(() => {
            // 普通DOM对象
            console.log($imgListElm.children)

            // Vue组件对象
            imgBtn.primary = 'warning'
        })

        // 记得return要把变量都暴露出去
        return {
            $imgListElm,
            imgBtn
        }
    }
}
  • 除此之外,还 允许以:ref的方式进行动态的绑定引用。 当然,不一定非要用ref()来接收,也可以使用reactive()来接收一个键值对的集合,这样的话,我们就可以像当初通过id操作jQuery对象的方式一样,直接获取到具体的DOM对象。
<template>
  <ul>
    <!-- 动态ref属性的值是一个JS执行函数,就是对引用进行  赋值  -->
    <li v-for="user in list" 
    :ref="el => { if (el) userRefMap['ref_' + user.oid] = el }">
        {{ user.name }}
    </li>
  </ul>
</template>

<script>
  import { reactive, toRefs } from 'vue'

  export default {
    setup() {
        // 响应数据
        const state = reactive({
            // ref映射对象
            userRefMap: {},
            // user数组
            list: [
                {
                    oid: 1001,
                    name: 'huilin'
                },
                {
                    oid: 1002,
                    name: 'fafa'
                },
                {
                    oid: 1003,
                    name: 'qiqi'
                }
            ]
        })

        onMounted(() => {
            // 输出DOM
            console.log(state.userRefMap['ref_1001'])
        })

        return {
            ...toRefs(state),
        }
    }
  }
</script>

ref模板引用的案例实践

实现效果

选中菜品后,进度条渐进加载,2秒后完成确认。同时,允许随时取消选中,再重新进行划菜。

主要思路

  1. 菜品<div class="dish-item">下,要有个<div class="checked-mask"/>遮罩层。通过改变遮罩层的样式style="width: 100%",同时设置transition过渡动画,实现渐进加载效果。
  2. click监听事件中,通过ref引用来操作遮罩层DOM对象。

这里可能有人会问:为什么不直接设置动态的:id属性,以document.getElementById()来操作DOM? 这是因为JS和DOM是彼此不相通的孤岛,每次访问DOM API都要过一次桥,频繁的过桥会有性能损耗,既然Vue提供了ref的方式,我们尽量还是遵循这个游戏规则

核心代码

<div v-for="dish in dishList" 
    :key="dish.oid" 
    :class="{checked : dish.submitted != -1}"
    @click="dishSubmit(dish)" class="dish-item">
    <!-- 遮罩层 -->
    <div :ref="el => { if(el) dishElmMap['id_' + dish.oid] = el}" 
        class="checked-mask" 
        v-show="dish.submitted != -1"></div>
    <!-- 2秒后确认的icon -->
    <van-icon :style="{visibility: dish.submitted == 1 ? 'visible' : 'hidden', opacity: dish.submitted == 1 ? 1 : 0}" 
        name="checked" 
        class="checked-icon" 
        size="30" />

    <!-- 菜品名称 -->
    <span class="name text-ellipsis">{{dish.name}}</span>
    <!-- 客户备注 -->
    <span class="tip text-ellipsis" style="text-align: right;">{{dish.remark}}</span>
</div>
import { reactive, toRefs, nextTick } from 'vue'

export default {
    name: 'Order',
    setup() {
        // 响应数据
        let state = reactive({
            dishElmMap: {},
            dishList: [{
                name: '干贝苦瓜羹',
                remark: '苦瓜熬久一点',
                submitted: -1,
                percent: 0,
                oid: 332312
                },
                {
                name: '周麻婆泡菜鱼',
                remark: null,
                submitted: -1,
                percent: 0,
                oid: 332313
                },
                {
                name: '酸辣土豆丝',
                remark: '变态辣',
                submitted: -1,
                percent: 0,
                oid: 332314
                },
                {
                name: '蒜蓉粉丝金针菇',
                remark: null,
                submitted: -1,
                percent: 0,
                oid: 332315
                },
                {
                name: '飘香馋嘴蛙',
                remark: null,
                submitted: -1,
                percent: 0,
                oid: 332316
                },
                {
                name: '麻婆田鸡王',
                remark: null,
                submitted: -1,
                percent: 0,
                oid: 332317
                },
                {
                name: '怀旧水煮鱼',
                remark: null,
                submitted: -1,
                percent: 0,
                oid: 332318
                },
                {
                name: '一品香脆虾',
                remark: null,
                submitted: -1,
                percent: 0,
                oid: 332319
                }
            ]
        }]

      // 菜品提交
      let dishSubmit = (dish) => {
        // 获取具体的DOM
        let elm = state.dishElmMap['id_' + dish.oid]
        
        
        if (dish.submitted === -1) {
          // 要选中时

          // -1未划菜;0-划菜中;1-已确认划菜
          dish.submitted = 0    
          // 设置DOM.style.width = 100%
          nextTick(() => {
            dish.animateTimer = setTimeout(() => {
              elm.style.width = '100%';
            },500)
          })

          if(dish.timer){
            clearTimeout(dish.timer)
            clearTimeout(dish.animateTimer)
          }
          // 3秒后修改状态为已划菜
          dish.timer = setTimeout(() => {
            dish.submitted = 1
            clearTimeout(dish.timer)
            clearTimeout(dish.animateTimer)
          }, 2000)
        } else {
          // 其他情况回退到初始状态
          dish.submitted = -1
          nextTick(() => {
            setTimeout(() => {
              elm.style.width = '0%';
            },0)
          })
          clearTimeout(dish.timer)
          clearTimeout(dish.animateTimer)
        }
      }        
    }
}

参考资料