vue3/vue2自定义指令看这一篇就够啦~(包含一些实用的自定义指令)

前言:

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。

Vue2

注册全局自定义指令

main.js

import Vue from 'vue'
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

注册局部指令

index.vue

<template>
  ...
</template>

<script>
export default {
  data() {
    return {};
  },
  directives: {
    focus: {
      // 指令的定义
      inserted: function (el) {
        el.focus();
      },
    },
  },
};
</script>

使用自定义指令

<template>
  <input v-model="value"  v-focus>
</template>

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:

        name:指令名,不包括 v- 前缀。

        value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

        oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。

                        无论值是否改变都可用。

        expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。

        arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。

        modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符

                        对象为 { foo: true, bar: true }。

  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

例:

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {
  bind: function (el, binding, vnode) {
    var s = JSON.stringify
    el.innerHTML =
      'name: '       + s(binding.name) + '<br>' +
      'value: '      + s(binding.value) + '<br>' +
      'expression: ' + s(binding.expression) + '<br>' +
      'argument: '   + s(binding.arg) + '<br>' +
      'modifiers: '  + s(binding.modifiers) + '<br>' +
      'vnode keys: ' + Object.keys(vnode).join(', ')
  }
})

new Vue({
  el: '#hook-arguments-example',
  data: {
    message: 'hello!'
  }
})

效果:

Vue3

注册全局自定义指令

简化形式

对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:

<div v-color="color"></div>

main.js

const app = createApp({})

app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

非简化形式

main.js

const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('color', {
   mounted(el, binding, vnode) {
        el.style.color = binding.value
    },
})

注册局部指令

index.vue

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

vue2、vue3的生命周期对照

钩子函数

Vue.js 2

Vue.js 3

bind

绑定时执行的逻辑

移除,使用beforeMount替代

inserted

元素插入到父节点时执行的逻辑

移除,可以使用mounted来代替

update

元素所在组件的VNode更新时执行的逻辑

使用beforeUpdate和updated替代

componentUpdated

元素所在组件的VNode及其子VNode更新时执行的逻辑

使用beforeUpdate和updated替代

unbind

解绑时执行的逻辑

使用unmounted替代

vue3自定义指令七个钩子

  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {},
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {}

钩子参数

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。

        value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。

        oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。

        arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。

        modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,

                        修饰符对象是 { foo: true, bar: true }。

        instance:使用该指令的组件实例。

        dir:指令的定义对象。

  • vnode:代表绑定元素的底层 VNode。
  • prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

实用的指令(以vue3为例)

waterMarker指令(水印)

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

function addWaterMarker(str, parentNode, font, textColor) {
  // 水印文字,父元素,字体,文字颜色
  var can = document.createElement("canvas");
  can.width = 200;
  can.height = 150;
  var cans = can.getContext("2d");
  cans.rotate((-20 * Math.PI) / 180);
  cans.font = font || "16px Microsoft JhengHei";
  cans.fillStyle = textColor || "rgba(180, 180, 180, 0.3)";
  cans.textAlign = "left";
  cans.textBaseline = "Middle";
  cans.fillText(str, can.width / 10, can.height / 2);
  parentNode.style.backgroundImage = "url(" + can.toDataURL("image/png") + ")";
}
app.directive("waterMarker", {
  beforeMount(el, binding) {
    addWaterMarker(
      binding.value.text,
      el,
      binding.value.font,
      binding.value.textColor
    );
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container" v-waterMarker="{text:'零凌林',textColor:'rgba(0, 0, 0, 0.4)'}">新页面</div>
</template>

<script setup>
import { ref, reactive } from "vue";
</script>

<style scoped>
.container{
    width: 100%;
    height: 98vh;
    border: 1px solid skyblue;
}
</style>

效果:

copy指令(点击复制)

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.directive("copy", {
  beforeMount(el, { value }) {
    el.$value = value;
    el.handler = () => {
      if (!el.$value) {
        console.log("复制内容为空");
        return;
      }
      const textarea = document.createElement("textarea");
      textarea.readOnly = "readonly";
      textarea.style.position = "absolute";
      textarea.style.left = "-9999px";
      textarea.value = el.$value;
      document.body.appendChild(textarea);
      textarea.select();
      if (navigator.clipboard) {
        navigator.clipboard.writeText(el.$value).then(() => {
          console.log("复制成功", el.$value);
        });
      } else {
        // execCommand即将被废弃
        const result = document.execCommand("Copy");
        if (result) {
          console.log("复制成功", el.$value);
        }
      }
      document.body.removeChild(textarea);
    };
    el.addEventListener("click", el.handler);
  },
  updated(el, { value }) {
    el.$value = value;
  },
  unmounted(el) {
    el.removeEventListener("click", el.handler);
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container">
    <input type="text" v-model="name" />
    <div>{{ name }}</div>
    <button v-copy="name">点击复制</button>
  </div>
</template>

<script setup>
import { ref, reactive } from "vue";
const name = ref("");
</script>

<style scoped></style>

效果:

clickOutside指令(点击元素外部)

tips:可用于点击弹窗以外范围关闭弹窗

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.directive("click-outside", {
  mounted(el, binding) {
    el.clickOutsideEvent = function (event) {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event);
      }
    };
    document.addEventListener("click", el.clickOutsideEvent);
  },
  unmounted(el) {
    document.removeEventListener("click", el.clickOutsideEvent);
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container">
    <div class="box" v-click-outside="handleClickOutside"></div>
  </div>
</template>

<script setup>
const handleClickOutside = (event) => {
  console.log('点击了box的外部');
};
</script>

<style scoped>
.box {
  width: 100px;
  height: 100px;
  border: 1px solid red;
}
</style>

效果:

longPress指令(长按)

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.directive("longPress", {
  beforeMount: function (el, binding, vNode) {
    if (typeof binding.value !== "function") {
      throw "callback must be a function";
    }
    // 定义变量
    let pressTimer = null;
    // 创建计时器( 1秒后执行函数 )
    let start = (e) => {
      if (e.type === "click" && e.button !== 0) {
        return;
      }
      if (pressTimer === null) {
        pressTimer = setTimeout(() => {
          handler();
        }, 1000);
      }
    };
    // 取消计时器
    let cancel = (e) => {
      if (pressTimer !== null) {
        clearTimeout(pressTimer);
        pressTimer = null;
      }
    };
    // 运行函数
    const handler = (e) => {
      binding.value(e);
    };
    // 添加事件监听器
    el.addEventListener("mousedown", start);
    el.addEventListener("touchstart", start, { passive: true });
    // 取消计时器
    el.addEventListener("click", cancel);
    el.addEventListener("mouseout", cancel);
    el.addEventListener("touchend", cancel);
    el.addEventListener("touchcancel", cancel);
  },
  // 当传进来的值更新的时候触发
  beforeUpdate(el, { value }) {
    el.$value = value;
  },
  // 指令与元素解绑的时候,移除事件绑定
  unmounted(el) {
    el.removeEventListener("click", el.handler);
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container">
    <div class="box" v-longPress="handleLongPress"></div>
  </div>
</template>

<script setup>
const handleLongPress = () => {
  console.log('触发了红box的长按')
}
</script>

<style scoped>
.box {
  width: 100px;
  height: 100px;
  border: 1px solid red;
}
</style>

效果:

banRightClick指令(禁止右键点击)

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.directive("banRightClick", {
  beforeMount: function (el, binding, vnode) {
    // 绑定事件处理程序
    el.addEventListener("contextmenu", function (event) {
      event.preventDefault(); // 阻止默认的右键菜单显示
      binding.value(event); // 调用指令绑定的回调函数,并传入鼠标事件对象
    });
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container">
    <div class="box" v-banRightClick="handleClickRight"></div>
  </div>
</template>

<script setup>
const handleClickRight = (e) => {
  console.log('右键被禁止',e)
}
</script>

<style scoped>
.box {
  width: 100px;
  height: 100px;
  border: 1px solid red;
}
</style>

效果:

 

rightClickMenu指令(右键点击菜单)

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.directive("rightClickMenu", {
  beforeMount: function (el, binding, vnode) {
    el.addEventListener("contextmenu", function (event) {
      event.preventDefault();

      if (binding.instance.menuVisible) {
        return false; // 如果菜单已经显示,则不再重复弹出
      }
      binding.instance.menuVisible = true; // 设置菜单为显示状态

      const menuItems = binding.value || [];

      let menu = document.createElement("ul");
      menu.style.position = "fixed";
      menu.style.zIndex = 999;
      menu.style.top = event.clientY + "px";
      menu.style.left = event.clientX + "px";

      menuItems.forEach((item) => {
        const menuItem = document.createElement("li");
        menuItem.innerText = item.text;
        menuItem.addEventListener("click", function () {
          binding.instance.menuVisible = false; // 点击菜单项后隐藏菜单
          item.handler();
        });
        menu.appendChild(menuItem);
      });
      document.body.appendChild(menu);

      function deleteMenu(e) {
        if (!el.contains(e.target)) {
          binding.instance.menuVisible = false;
          if (menu) document.body.removeChild(menu);
          menu = null;
        }
      }
      // 点击其他地方时隐藏菜单
      el.__vueDeleteMenu__ = deleteMenu;
      document.addEventListener("click", deleteMenu);
    });
  },
  unmounted(el) {
    document.removeEventListener("click", el.__vueDeleteMenu__);
    delete el.__vueDeleteMenu__;
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container">
    <div class="box" v-rightClickMenu="menuItems"></div>
  </div>
</template>

<script setup>
const menuItems = [
    { id: 1, text: '菜单项1', handler: () => console.log('点击了菜单项1') },
    { id: 2, text: '菜单项2', handler: () => console.log('点击了菜单项2') },
    { id: 3, text: '菜单项3', handler: () => console.log('点击了菜单项3') }
  ]
</script>

<style scoped>
.box {
  width: 100px;
  height: 100px;
  border: 1px solid red;
}
</style>

效果:

highlightText指令(文本高亮)

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

function setHighlight(el, binding) {
  const { value } = binding;
  let _keywords = value;
  let _color = "red";
  if (typeof value === "object") {
    const { keywords = "", color = _color } = value;
    _keywords = keywords;
    _color = color;
  }
  if (_keywords) {
    _keywords = _keywords.split("|");
    const regex = new RegExp(`(${_keywords.join("|")})`, "gi");
    const highlightedText = el.innerText.replace(
      regex,
      `<span style="color: ${_color};">$1</span>`
    );
    el.innerHTML = highlightedText;
  } else {
    el.innerHTML = el.innerText;
  }
}

app.directive("highlightText", {
  beforeMount: function (el, binding) {
    setHighlight(el, binding);
  },
  updated: function (el, binding) {
    setHighlight(el, binding);
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container">
    <input type="text" v-model="content">
    <div v-highlightText="content">内容:123456789abc</div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const content = ref('')
</script>

效果:

resize指令(监听元素尺寸变化)

main.js

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

/* 元素大小发生变化的回调,监听窗口大小 */
app.directive("resize", {
  mounted(el, binding) {
    const resizeObserver = new ResizeObserver((entries) => {
      for (let entry of entries) {
        binding.value(entry);
      }
    });
    resizeObserver.observe(el);
    el._resizeObserver = resizeObserver;
  },
  unmounted(el) {
    el._resizeObserver.disconnect();
  },
});

app.mount("#app");

App.vue

<template>
  <div class="container">
    <div class="box" v-resize="handleSizeChange"></div>
  </div>
</template>

<script setup>
const handleSizeChange = () => {
  console.log('红box大小发生变化了')
}
</script>

<style scoped>
.box {
  width: 100%;
  height: 100px;
  border: 1px solid red;
}
</style>

效果:

拓展:

边界情况:访问组件实例

通常来说,建议在组件实例中保持所使用的指令的独立性。从自定义指令中访问组件实例,通常意味着该指令本身应该是一个组件。然而,在某些情况下这种用法是有意义的。

在 Vue 2 中,必须通过 vnode 参数访问组件实例:

bind(el, binding, vnode) {
  const vm = vnode.context
}

在 Vue 3 中,实例现在是 binding 参数的一部分:

mounted(el, binding, vnode) {
  const vm = binding.instance
}
WARNING
有了片段的支持,组件可能会有多个根节点。当被应用于多根组件时,自定义指令将被忽略,并将抛出警告。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

零凌林

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值