Vue自定义指令从入门到实用:自动聚焦、权限控制、防抖、懒加载……全案例教学

一、啥是自定义指令?凭啥要用它?

Vue 自带了一些指令,比如 v-modelv-ifv-showv-for,这些大家天天用。

但有时候,你想给元素加一个"自动聚焦"的行为,可能会在 onMounted 里写 input.focus()。如果有好几个页面都要这个功能,你就得把那段代码复制来复制去,又丑又容易漏。

这时候,自定义指令就派上用场了。你可以自己造一个 v-focus,然后往任何元素上一写,它就自动聚焦,一句废话都没有。

一句话总结: 自定义指令就是让你把"操作 DOM 的逻辑"封装成一个个可复用的"标记"。


二、先写一个最基础的局部自定义指令

局部指令就是只在当前组件里能用,定义在 <script setup> 里面。我们用自动聚焦来开篇。

案例1:页面打开,输入框自动获得焦点

vue

复制代码
<template>
  <div>
    <h3>自动聚焦输入框</h3>
    <!-- 
      v-focus 是我们自定义的指令
      名字前面有个 v,但定义的时候只叫 focus 
    -->
    <input v-focus type="text" placeholder="我一出来就聚焦" />
  </div>
</template>

<script setup>
// 定义一个局部自定义指令,名字叫 focus
// 注意:定义时用小写 v 开头,但要写成 vFocus 这种驼峰,模板里用 v-focus
const vFocus = {
  // mounted 是钩子函数,当元素被挂载到 DOM 上时调用
  mounted(el) {
    // el 就是被绑定了这个指令的原生 DOM 元素,这里就是 input
    el.focus()  // 直接调用原生 focus 方法
  }
}
</script>

拆开来讲:

  • 指令对象里可以定义多个钩子函数 ,最常用的就是 mounted,它在元素插入 DOM 后触发。

  • mounted(el) 里的 el 就是绑定了这个指令的原生 DOM 元素,你可以对它做任何原生操作。

  • 为啥叫 vFocus?因为 Vue 会自动把模板里的 v-focus 和脚本里的 vFocus 关联起来。这是 Vue 的约定。


三、全局自定义指令:一次定义,到处都能用

局部指令只能在当前组件用,很多实用功能我们希望在整个应用里都能直接用,这就需要全局指令。

main.js 里用 app.directive 注册。

案例2:全局自动聚焦指令

main.js

javascript

复制代码
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 注册全局指令,名字叫 focus
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

app.mount('#app')

之后,任何组件 里都可以直接写 <input v-focus />,不用再单独引入。


四、指令的钩子函数和参数

指令有好几个钩子,跟组件的生命周期有点像,但专门针对绑定了指令的这个元素。

常用钩子:

  • created:元素创建后,但还没插入 DOM(很少用)。

  • mounted:元素插入 DOM 后(最常用)。

  • updated:组件更新导致 DOM 变化后。

  • beforeUnmount:元素被移除前。

钩子函数可以拿到三个参数:

  • el:绑定的元素本身。

  • binding:一个对象,包含指令相关的信息,比如 value(指令的值)、arg(参数)、modifiers(修饰符)。

  • vnode:虚拟节点(一般用不到)。

用一张表记住 binding:

属性 说明 示例 v-demo:arg.modifier="value"
binding.value 指令的值 "value"
binding.arg 冒号后面的参数 "arg"
binding.modifiers 修饰符对象 { modifier: true }

我们用几个案例把这三个东西玩明白。


五、带参数和值的指令

案例3:改变背景色指令 v-bg

需求:给元素加一个背景色,颜色通过指令值传入。还可以用参数指定是背景色还是文字颜色。

vue

复制代码
<template>
  <div>
    <!-- 
      使用 v-bg:background="'lightblue'" 
      参数 background 表示改背景色
      值 'lightblue' 是颜色
    -->
    <p v-bg:background="'lightblue'">我的背景是浅蓝色</p>

    <!-- 参数 color 表示改文字颜色 -->
    <p v-bg:color="'red'">我的文字是红色</p>

    <!-- 不写参数,默认改背景色 -->
    <p v-bg="'yellow'">默认改背景色为黄色</p>
  </div>
</template>

<script setup>
const vBg = {
  mounted(el, binding) {
    // binding.arg 是参数(冒号后面的东西)
    // binding.value 是指令的值(等号后面的东西)
    const arg = binding.arg || 'background'  // 没传参数就默认改背景色
    
    if (arg === 'background') {
      el.style.backgroundColor = binding.value
    } else if (arg === 'color') {
      el.style.color = binding.value
    }
  }
}
</script>

关键点: binding.arg 拿到参数(backgroundcolor),binding.value 拿到颜色值。这样你就能用一个指令干两件事。


六、带修饰符的指令

修饰符就是点后面的东西,比如 v-on:click.prevent 里的 .prevent

案例4:防抖点击指令 v-debounce

需求:有些按钮不能猛点(比如支付按钮),我们需要点击后一定时间内不能再次点击。用修饰符 .lock 控制是否"锁定"按钮。

vue

复制代码
<template>
  <div>
    <!-- 
      使用 v-debounce.lock="handlePay" 
      .lock 修饰符表示点击后锁定按钮 1 秒,防止重复点击
      值 handlePay 是点击后要执行的回调函数
    -->
    <button v-debounce.lock="handlePay">支付(锁定1秒)</button>
    
    <!-- 不加 .lock 修饰符,就是普通点击,不做防抖 -->
    <button v-debounce="handleNormal">普通按钮</button>
  </div>
</template>

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

const vDebounce = {
  mounted(el, binding) {
    // 防抖时间,写死 1 秒,也可以做成指令参数
    const delay = 1000
    let timer = null
    
    // 检查修饰符里有没有 lock
    const needLock = binding.modifiers.lock

    // 给元素绑定点击事件
    el.addEventListener('click', () => {
      if (needLock && timer) {
        // 如果有 lock 修饰符且定时器还在,说明还没解禁,直接返回
        return
      }

      // 执行传进来的回调函数
      if (typeof binding.value === 'function') {
        binding.value()
      }

      if (needLock) {
        // 按钮禁用样式
        el.disabled = true
        // 设置定时器,1 秒后解禁
        timer = setTimeout(() => {
          el.disabled = false
          timer = null
        }, delay)
      }
    })
  }
}

function handlePay() {
  console.log('支付请求已发送,按钮已锁定1秒')
  // 实际项目这里发请求
}

function handleNormal() {
  console.log('普通点击')
}
</script>

解析:

  • binding.modifiers.locktrue 时,我们用 setTimeout 锁定按钮。

  • binding.value 是传进来的函数,我们直接调用它。

  • 这个指令把"防抖"逻辑完全封装了起来,模板里看起来非常干净。


七、指令也能接收对象类型的值

上面传的都是简单值,其实 binding.value 可以是任意类型,包括对象。这样就能传多个参数。

案例5:权限控制指令 v-permission

需求:根据用户权限决定某个元素是否显示。我们假设当前用户角色是 'editor',只有角色匹配时才显示元素。

vue

复制代码
<template>
  <div>
    <!-- 
      传一个对象给指令,包含角色列表和是否移除
      roles: ['admin', 'editor'] 表示这两个角色可见
      remove: true 表示没有权限时直接移除DOM(否则只是隐藏)
    -->
    <button v-permission="{ roles: ['admin'], remove: false }">
      只有管理员可见(隐藏方式)
    </button>
    
    <button v-permission="{ roles: ['admin'], remove: true }">
      只有管理员可见(移除方式)
    </button>
    
    <!-- 当前用户是 editor,所以上面两个按钮都不可见,下面这个可见 -->
    <button v-permission="{ roles: ['admin', 'editor'] }">
      管理员和编辑可见
    </button>
  </div>
</template>

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

// 假设当前用户角色是 editor
const currentRole = ref('editor')

const vPermission = {
  mounted(el, binding) {
    const { roles = [], remove = false } = binding.value || {}
    // 检查当前角色是否在允许的角色列表里
    const hasPermission = roles.includes(currentRole.value)
    
    if (!hasPermission) {
      if (remove) {
        // 直接移除 DOM 元素
        el.parentNode?.removeChild(el)
      } else {
        // 或者隐藏
        el.style.display = 'none'
      }
    }
  }
}
</script>

说明:

  • binding.value 拿到了整个对象 { roles: [...], remove: true }

  • 我们可以解构出来,然后根据逻辑决定显示、隐藏还是移除。


八、综合实战案例:图片懒加载指令 v-lazy

这是前端里非常实用的一个功能:页面上的图片先不加载,等它滚到可视区域了再加载,节省带宽和提升性能。

vue

复制代码
<template>
  <div>
    <h3>往下滚,图片会懒加载</h3>
    <!-- 占位高度,让页面能滚 -->
    <div style="height: 800px; background: #f5f5f5; text-align: center; line-height: 800px;">
      请向下滚动
    </div>
    
    <!-- 
      v-lazy 指令,值传图片的真实地址
      初始时先显示一张占位图(可以是透明的或 loading 图)
    -->
    <img v-lazy="'https://picsum.photos/id/1/400/300'" alt="懒加载图片1" />
    <br />
    <img v-lazy="'https://picsum.photos/id/2/400/300'" alt="懒加载图片2" />
    <br />
    <img v-lazy="'https://picsum.photos/id/3/400/300'" alt="懒加载图片3" />
  </div>
</template>

<script setup>
const vLazy = {
  mounted(el, binding) {
    // 先设置占位图(一张灰色的小图或 loading 图)
    el.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjZGRkIiAvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VlZSIgLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+'
    
    // 真实图片地址存起来
    const realSrc = binding.value
    
    // 创建 IntersectionObserver 监听元素是否进入可视区域
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // 进入可视区域了,加载真实图片
          el.src = realSrc
          // 加载完就停止监听
          observer.unobserve(el)
        }
      })
    }, {
      rootMargin: '0px 0px 100px 0px'  // 提前 100px 就开始加载
    })
    
    // 开始观察这个元素
    observer.observe(el)
    
    // 组件卸载时也清理 observer(这里简化处理,一般会存下来在 unmounted 里断开)
    el._observer = observer
  },
  unmounted(el) {
    // 清理观察器
    if (el._observer) {
      el._observer.disconnect()
    }
  }
}
</script>

核心原理:

  • 利用浏览器原生的 IntersectionObserver API,监听元素是否进入视口。

  • 进入时把占位图的 src 换成真实地址。

  • 同时做了卸载时的清理工作。

这个指令可以直接用到任何项目里,非常实用。


九、总结一下自定义指令的写法

步骤 说明
1. 定义 const vXxx = { mounted(el, binding) { ... } }
2. 使用 <div v-xxx="值"></div>
3. 参数 通过 binding.arg
4. 修饰符 通过 binding.modifiers
5. 值 通过 binding.value 拿,可以是任意类型

局部注册:<script setup> 里定义 vXxx 对象就行,模板自动识别。

全局注册:main.jsapp.directive('xxx', { ... })


十、什么时候用组件,什么时候用指令?

这个问题很多新手会纠结,简单给个判断标准:

  • 如果需要一段 HTML 模板 + 逻辑 + 样式 ,用组件

  • 如果只是对某个 DOM 元素做一点操作 (改样式、加事件、调焦点),用自定义指令更轻量。

比如权限隐藏,用组件包装也可以,但如果只是加个判断,一个指令就搞定,不用多套一层 div。

相关推荐
嘟嘟07171 小时前
吃透 JS 八大数据类型与内存原理,从代码到底层一站式复习
前端
问心无愧05131 小时前
ctf show web入门157 158
前端·笔记
该用户已成仙1 小时前
vue3 使用 vuedraggable 报错 TypeError: isFunction2 is not a function
前端·javascript·vue.js
aidou13141 小时前
Kotlin中实现星级评价选择功能(仅支持整数)
前端·kotlin·自定义view·imageview·ontouchevent·customratingbar
良逍Ai出海1 小时前
我用 Codex 搭了一个 WordPress 独立站
前端
TPBoreas1 小时前
前端面试问题打把-场景题
开发语言·前端·javascript
问心无愧05131 小时前
ctf show web入门159
前端·笔记
San813_LDD1 小时前
[Vue/HTML]ECharts 使用指南:从入门到绘制各种常用图表
vue.js·html·echarts
恋猫de小郭1 小时前
Flutter 又为 AI 时代添砖加瓦:全新 ComponentLibrary 提议
android·前端·flutter