×

Vue中的自定义指令

独孤求败 独孤求败 发表于2026-07-07 09:42:32 浏览34 评论0

抢沙发发表评论

我们之前介绍的 v-ifv-for 等指令,都是 Vue 提供的内置指令。

除此之外,Vue 还支持自定义指令,允许我们像使用内置指令一样,把一些 DOM 操作逻辑封装成可复用的指令,从而实现更灵活的页面行为控制。

我们前面讲过组件,组件是用来消除重复代码、提升复用性的有效方式之一。其实,自定义指令同样也能实现代码复用,只不过它更偏向于对 DOM 行为的抽象,而组件则更侧重于结构和视图的复用,两者在使用方式和适用场景上有明显区别。

例子

接下来我们会通过示例自定义指令 focus 和 color 来进行演示。它们需要通过 app.directive() 进行注册,在模板中使用时,则需要在指令名称前加上 v- 前缀。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Vue 3 全局引入自定义指令示例</title>
    <!-- 引入 Vue 3 -->
    <script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
    <style>
        body { font-family: sans-serif; padding20px; }
        .box { padding10pxmargin10px 0border-radius4pxcolor: white; }
    
</style>
</head>
<body>

    <div id="app">
        <h2>1. 自动聚焦指令 (v-focus)</h2>
        <!-- 页面加载时,这个输入框会自动获取焦点 -->
        <input v-focus type="text" placeholder="我自动获取了焦点...">

        <hr>

        <h2>2. 传参自定义指令 (v-color)</h2>
        <!-- 使用自定义指令改变背景色 -->
        <div v-color="'#3498db'" class="box">我是蓝色背景</div>
        <div v-color="'#e74c3c'" class="box">我是红色背景</div>
    </div>

    <script>
        // 从全局 Vue 对象中解构出 createApp
        const { createApp } = Vue;

        const app = createApp({
            setup() {
                return {};
            }
        });

        // ==================== 注册自定义指令 ====================

        // 示例 1: v-focus (当页面加载时自动聚焦)
        app.directive('focus', {
            // mounted 是指令的生命周期钩子,代表元素被插入到 DOM 中时执行
            mounted(el) {
                el.focus();
            }
        });

        // 示例 2: v-color (根据传入的值改变元素背景色)
        app.directive('color', {
            // binding 对象可以获取到指令传入的值 (binding.value)
            mounted(el, binding) {
                el.style.backgroundColor = binding.value;
            },
            // updated 钩子确保当响应式数据改变时,指令也能更新样式
            updated(el, binding) {
                el.style.backgroundColor = binding.value;
            }
        });

        // ========================================================

        // 挂载应用
        app.mount('#app');
    
</script>

</body>
</html>

两种注册方式

如上面例子所示,使用app.directive()注册的叫全局注册指令

另一种方式是局部注册指令,它通过组件选项中的 directives 来定义,而不再使用 app.directive() 进行全局注册。接下来我们用同样的示例,在组件内部实现一版局部指令的写法。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Vue 3 全5 局部注册自定义指令示例</title>
    <!-- 引入 Vue 3 -->
    <script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
    <style>
        body { font-family: sans-serif; padding20px; }
        .box { padding10pxmargin10px 0border-radius4pxcolor: white; }
    
</style>
</head>
<body>

    <div id="app">
        <h2>1. 局部自动聚焦指令 (v-focus)</h2>
        <input v-focus type="text" placeholder="我自动获取了焦点...">

        <hr>

        <h2>2. 局部传参自定义指令 (v-color)</h2>
        <div v-color="'#2ecc71'" class="box">我是绿色背景</div>
        <div v-color="'#9b59b6'" class="box">我是紫色背景</div>
    </div>

    <script>
        const { createApp } = Vue;

        // 1. 先定义指令对象(也可以直接写在组件内,提出来写代码更清晰)
        const focusDirective = {
            mounted(el) {
                el.focus();
            }
        };

        const colorDirective = {
            mounted(el, binding) {
                el.style.backgroundColor = binding.value;
            },
            updated(el, binding) {
                el.style.backgroundColor = binding.value;
            }
        };

        // 2. 创建应用并在根组件中【局部注册】
        createApp({
            setup() {
                // 这里可以写你的响应式数据和逻辑
                return {};
            },
            // 使用 directives 选项进行局部注册
            directives: {
                // 键名就是指令名(不需要加 v-)
                focus: focusDirective,
                color: colorDirective
            }
        }).mount('#app');
    
</script>

</body>
</html>

全局注册指令和局部注册指令不仅在使用方式上不同,更重要的是它们的作用域存在差异。

全局注册:在整个项目范围内都可以直接使用该指令。

局部注册:如果在 A 组件中声明了该指令,那么它只能在 A 组件的模板中使用;若其他组件需要使用,则必须在各自组件中重新声明。并且局部指令不具备“自动继承”特性,即使在父组件中定义,子组件也无法直接使用,仍需单独注册。

生命周期

同样组件一样,指令也有完整的声明周期,钩子函数如下:

const myDirective = {
// 1. 在绑定元素的 attribute 前或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {},

// 2. 在元素被插入到 DOM 前调用(此时可以做一些不依赖 DOM 树的准备工作)
beforeMount(el, binding, vnode, prevVnode) {},

// 3. 💥 最常用:在绑定元素的父组件及它自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},

// 4. 在绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},

// 5. 💥 最常用:在绑定元素的父组件及它自己的所有子节点都更新完成后调用
updated(el, binding, vnode, prevVnode) {},

// 6. 在绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},

// 7. 在绑定元素从 DOM 中移除且父组件卸载后调用(通常用于清理定时器、解绑事件)
unmounted(el, binding, vnode, prevVnode) {}
}

综合来看,这些声明周期函数主要接收如下四个参数

  1. 1. el: 指令绑定的具体的真实DOM元素,可以直接用来操作DOM
  2. 2. binding:一个对象,包含以下重要属性:
    • • value:传递给指令的值(例如 v-my-dir="1 + 1",值就是 2)。
    • • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。
    • • arg:传递给指令的参数(例如 v-my-dir:foo,参数就是 "foo")。
    • • modifiers:一个包含修饰符的对象(例如 v-my-dir.modifiers,对象是 { modifiers: true })。
  3. 3. vnode 代表真实 DOM 元素的虚拟节点。
  4. 4. prevVnode 上一个虚拟节点,仅在 beforeUpdate 和 updated 钩子中可用。

指令参数

在前面介绍自定义指令生命周期时,我们提到过 binding.arg 参数。实际上,binding.arg 就是指令参数(Directive Argument),用于向指令传递额外的信息,使同一个指令能够根据不同的参数执行不同的逻辑。

它的典型使用场景包括:功能切换、动态指定样式属性、指定事件类型等。

如果你对这个概念还有些陌生,可以先回忆一下 Vue 的内置指令。例如:

<a v-bind:href="url">Vue 官网</a>

这里的 href 就是 v-bind 的指令参数,对应到自定义指令中,就是 binding.arg 的值。

接下来,我们通过一个简单的示例,直观地了解 binding.arg 的使用方式。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Vue 3 全局引入自定义指令示例</title>
    <!-- 引入 Vue 3 -->
    <script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
    <style>
        body { font-family: sans-serif; padding20px; }
        .box { padding10pxmargin10px 0border-radius4pxcolor: white; }
    
</style>
</head>
<body>
    <div id="app">
        <!-- 传入参数 top,代表修改 marginTop -->
        <div v-distance:top="20">距离顶部 20px</div>
        
        <!-- 传入参数 left,代表修改 marginLeft -->
        <div v-distance:left="50">距离左侧 50px</div>
        
        <!-- 还可以配合响应式数据,动态改变参数 -->
        <div v-distance:[direction]="300">动态方向距离 30px</div>
    </div>
    
    <script>
        const { createApp, ref } = Vue;
    
        createApp({
            setup() {
                // 动态参数:可以是 'top', 'bottom', 'left', 'right'
                const direction = ref('bottom'); 
                return { direction };
            },
            directives: {
                distance: {
                    mounted(el, binding) {
                        // binding.arg 拿到的就是冒号后面的值,比如 'top' 或 'left'
                        const styleStyle = binding.arg || 'top'
                        
                        // 首字母大写(如 top -> Top),拼出 marginTop
                        const camelCaseStyle = 'margin' + styleStyle.charAt(0).toUpperCase() + styleStyle.slice(1);
                        
                        // binding.value 拿到的是等号后面的值,比如 20 或 50
                        el.style[camelCaseStyle] = binding.value + 'px';
                    }
                }
            }
        }).mount('#app');
    
</script>

</body>
</html>


群贤毕至

访客