vue3重走来时路之:自定义指令知多少?

932 阅读7分钟

1、概述

我们都知道在vue中主要通过组件的方式来实现代码的复用和抽象。但是有些情况下,我们还是需要对普通的DOM元素进行底层操作,那这个时候我们就需要用的vue的指令系统来完成或实现不同的功能。

那vue指令除了内置了16个核心功能的指令以外,还允许我们创建自定义的指令(全局指令、局部指令)。

自定义指令也能帮助我们实现部分代码功能的复用。

2、使用方式

  • v-xxx:不带参数的指令
  • v-xxx="value":带变量值参数传入指令中
  • v-xxx="'string'":带字符串参数传入指令中
  • v-xxx:[argument]="value":增加动态argument参数到指令中
  • v-xxx:argument.modifier="value":增加修饰符到指令中 以上为我们常用的使用方式,具体细节我们在实现自定义指令中会具体这几种方式的使用。

3、vue内置的16个指令

  • v-text
  • v-html
  • v-pre
  • v-if
  • v-else
  • v-else-if
  • v-show
  • v-for
  • v-once
  • v-memo: 3.2版本新增指令
  • v-model
  • v-bind
  • v-on
  • v-slot
  • v-cloak
  • v-is : 已在 3.1.0 版本中被废弃。请换用带有 vue: 前缀的 is attribute。

下面我们来详细的介绍部分指令的作用。

v-text

<p v-text="msg"></p>
<!--  等价于 -->
<p>{{ msg }}</p>

v-html

<template>
  <div v-html="vHtml"></div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';

const vHtml = ref(
  '<h2>这是v-htmlh2标签啊</h2><h3 style="color: red;">这是v-htmlh3标签啊,红色字体</h3>'
);
</script>

v-pre

跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。

通俗点说:比如你显示{{ xxx }}这样的文字,如果带上v-pre指令,编译的时候就不会去解析这个节点,你原本是什么样的就给你展示成什么样的

<p v-pre>{{ this will not be compiled }}</p>

v-show

v-show该节点会一直存在,只是切换元素的 display

v-once

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

  • 自 3.2 开始,你还可以通过 v-memo 来记住带有失效条件的部分模板。

v-memo

3.2新增指令

记住一个模板的子树,元素和组件都可使用。该指令接受一个固定长度的数组作为依赖值进行记忆比对。如果数组中的每个值和上次渲染的时候相同,则整个该子树的更新都会被跳过。

v-memo 仅供性能敏感场景的针对性优化,会用到的场景应该很少。渲染 v-for 长列表 (长度大于 1000) 可能是它最有用的场景:

<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
</div>

const list = reactive([
  { name: '张三', id: 1 },
  { name: '李四', id: 2 },
  { name: '王二', id: 3 },
  { name: '赵五', id: 4 },
  { name: '曹六', id: 5 },
]);
const selected = ref(1);

当组件的 selected 状态发生变化时,即使绝大多数 item 都没有发生任何变化,大量的 VNode 仍将被创建。此处使用的 v-memo 本质上代表着“仅在 item 从未选中变为选中时更新它,反之亦然”。这允许每个未受影响的 item 重用之前的 VNode,并完全跳过差异比较。注意,我们不需要把 item.id 包含在记忆依赖数组里面,因为 Vue 可以自动从 item 的 :key 中把它推断出来。

v-model

该指令只能用于:inputtextareaselectcomponents这几个标签

修饰符有以下几种:

  • .lazy:把默认的监听 input 事件改为监听 change
  • .number:输入字符串转为有效的数字
  • .trim:去除输入的首尾空格
<input type="text" v-model.trim="value" placeholder="请输入" />

v-on

绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略。 缩写为:@[event]="xxx"

修饰符有以下几种:

  • .stop:阻止单击事件继续冒泡
  • .prevent:阻止浏览器默认行为
  • .capture:添加事件侦听器时使用事件捕获模式
  • .self:只执行直接作用在该元素身上的事件,会忽略其他元素的冒泡或者捕获事件
  • .once:事件只会触发一次
  • .passive:告诉浏览器不用去查询,我们没用preventDefault阻止默认行为
  • .{keyAlias}:仅当事件是从特定键触发时才触发回调。
  • .left:只当点击鼠标左键时触发。
  • .right:只当点击鼠标右键时触发。
  • .middle:只当点击鼠标中键时触发。

上面几个修饰符详细介绍可以看我另外一篇文章:修饰符详细介绍

<!-- 方法处理器 -->
<button v-on:click="doThis"></button>

<!-- 动态事件 -->
<button v-on:[event]="doThis"></button>

<!-- 内联语句 -->
<button v-on:click="doThat('hello', $event)"></button>

<!-- 缩写 -->
<button @click="doThis"></button>

<!-- 动态事件缩写 -->
<button @[event]="doThis"></button>

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form>

<!-- 串联修饰符 -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter" />

<!-- 点击回调只会触发一次 -->
<button v-on:click.once="doThis"></button>

<!-- 对象语法 -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

v-bind

动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。 缩写为: :. (当使用 .prop 修饰符时)

修饰符有以下几种:

  • .camel:将 kebab-case attribute 名转换为 camelCase。
  • .attr:将一个绑定强制设置为一个 DOM attribute。(3.2新增
  • .prop:将一个绑定强制设置为一个 DOM property。(3.2新增
<!-- 绑定 attribute -->
<img v-bind:src="imageSrc" />

<!-- 动态 attribute 名 -->
<button v-bind:[key]="value"></button>

<!-- 缩写 -->
<img :src="imageSrc" />

<!-- 动态 attribute 名缩写 -->
<button :[key]="value"></button>

<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName" />

<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]"></div>

<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>

<!-- 绑定一个全是 attribute 的对象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>

<!-- prop 绑定。"prop" 必须在 my-component 声明 -->
<my-component :prop="someThing"></my-component>

<!-- 将父组件的 props 一起传给子组件 -->
<child-component v-bind="$props"></child-component>

<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>

v-slot

提供具名插槽或需要接收 prop 的插槽。

插槽名 (可选,默认值是 default)

缩写为:#

适用于:template标签上面

<template #default></template>

v-cloak

这个指令保持在元素上直到关联组件实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕。 作用:为了防止在页面加载时先出现变量名闪烁的情况,造成不好的用户体验

例如:{{ msg }} (闪一下)=> hello

用法:

  • html中: {{ msg }}
  • css中[v-cloak] { display:none }

一定要搭配css一起用,一定要搭配css一起用,一定要搭配css一起用

<div v-cloak>{{ msg }}</div>
<style lang="scss">
[v-cloak] {
  display: none;
}
</style>

以上这些就是部分内置指令的介绍和用法。

4、自定义指令有哪些钩子函数?

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加在普通的 v-on 事件监听器调用前的事件监听器中时,这很有用。
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。
  • mounted:在绑定元素的父组件被挂载后调用。
  • beforeUpdate:在更新包含组件的 VNode 之前调用。
  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。
  • beforeUnmount:在卸载绑定元素的父组件之前调用。
  • unmount:当指令与元素解除绑定且父组件已卸载时,只调用一次。

5、那这些指令钩子函数传递了哪些参数呢?

  • el:指令绑定到的元素。这可用于直接操作 DOM。
  • binding:包含以下 property 的对象。
    • instace:使用指令的组件实例。
    • value:传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2
    • oldValue:先前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否有更改都可用。
    • arg:传递给指令的参数(如果有的话)。例如在 v-my-directive:foo 中,arg 为 "foo"
    • modifiers:包含修饰符(如果有的话) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true}
    • dir:一个对象,在注册指令时作为参数传递。
  • vnode:一个真实 DOM 元素的蓝图,对应上面收到的 el 参数。
  • prevNode:上一个虚拟节点,仅在 beforeUpdate 和 updated 钩子中可用。

6、注册自定义指令分为全局注册和局部注册

全局注册

app.directive('my-directive', {
  mounted: (el) => {
    el.style.background = '#4598d2';
  },
});

setup中局部注册

setup中局部注册自定义指令必须以 vNameOfDirective 的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。

// Directive是从vue导出的一个ts类型声明
const vMyDirective: Directive = {
  mounted: (el) => {
    el.style.background = 'red';
  },
};

7、我们来实现一个注册在全局的防抖切换背景色的自定义指令

  1. 首页我们在项目根目录创建文件夹: directives
  2. 然后在 directives 目录创建 index.tsbackground.tsdebounce.ts 三个文件
  3. index.tsbackground.tsdebounce.ts 三个文件代码如下:
// background.ts
import { Directive } from 'vue';

export const background: Directive = (el, binding) => {
  el.style.background = binding.value;
};

export default background;
// debounce.ts
import { Directive } from 'vue';

export const debounce: Directive = {
  mounted(el, binding) {
    let timer: number;
    el.addEventListener('click', () => {
      timer && clearTimeout(timer);
      timer = window.setTimeout(() => binding.value(), 300);
    });
  },
};

export default debounce;
// index.ts
import { App, Directive } from 'vue';
import debounce from '@/directives/debounce';
import background from '@/directives/background';

const directives = {
  debounce,
  background,
};

export default {
  install(app: App) {
    Object.keys(directives).forEach((key) => {
      // 这里是批量注册指令
      app.directive(key, (directives as { [key: string]: Directive })[key]);
    });
  },
};
  1. 然后我们在项目的 main.ts 代码中引入我们刚才创建的 index.ts,并通过 app.use()注册进去,代码如下:
import { createApp } from 'vue';
import App from './App.vue';

import directives from './directives';
const app = createApp(App);

app.use(directives).mount('#app');
  1. 至此,我们两个自定义指令就可以在组件中愉快的使用了。
  2. 使用方式如下:
// template
<el-button v-debounce="click">这里是防抖指令,快速点击试试:{{ debounceText }}</el-button>
<el-button v-background="background" style="color: white" @click="changeBackground">
  这里是设置背景色指令:{{ background }}
</el-button>
// setup
const debounceText = ref(new Date().getTime());
const click = () => (debounceText.value = new Date().getTime());

const background = ref('#4598d2');
const changeBackground = () =>
  (background.value = `#${('00000' + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)}`);
  1. 自定义指令的实现到此就结束了哦

8、结尾

推荐大家有空可以自己动手试试,毕竟百看不如一试加深体验。