2022年了!用过Vue的自定义指令吗?

5,784 阅读8分钟

前言

如今Vue占据了国内前端市场的大壁江山,在前端求职过程中,Vue的相关面试题也是越来越多。之所以Vue如此火热,很大部分原因就是它的渐进式、组件化、指令式等思想,让普通开发人员非常好上手。

指令是一个Vue项目用的最多的东西之一了,今天我们就来讲解Vue指令中的一个分支:自定义指令。

1. 什么是自定义指令?

想要使用自定义指令,首先我们得搞清楚什么是自定义指令?

自定义指令非常好理解,我们使用的v-for、v-if、v-model等等称之为指令,也被称之为Vue的内置指令。这些指令都是我们可以直接使用的。

为了更好的满足需求,最大化的让开发者个性化开发,Vue暴漏了自定义指令的API给我们,让我们除了使用内置指令外,我们还可以自己定义指令,定义好后和内置指令的方式非常类似。

比如我们看面的代码:

<p v-pin="200">我是一段话</p>

上面那段代码中v-pin可能很多小伙伴不知道是什么,看起来像是指令,但是有没有遇到过。其实v-pin就是一个自定义指令,只是我们这儿省略注册它的代码。

2. 环境准备

我们为了房间,直接使用Vue2.x的脚手架工具快速搭建一个项目。

搭建命令:

vue create 项目名称

运行起来:

3. 如何注册自定义指令?

要想使用自定义指令,我们必须先提前把它注册好,就好比我们的组件一样,得先注册,才能使用。

注册指令也分为全局注册和局部注册,和全局注册组件和局部注册组件一个道理。全局注册的指令可以在任何组件中直接使用,局部注册的指令只能在注册的地方使用。

3.1 全局注册

全局注册顾名思义,自定义指令注册好后,在项目的所有组件内都可以直接使用。

Vue提供了一个directive方法给我们注册自定义指令,我们在main.js中注册一个全局的自定义指令。

代码如下:

// src/main.js
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;
Vue.directive("resize", {
  bind() {},
  inserted() {},
  update() {},
  componentUpdated() {},
  unbind() {},
});
new Vue({
  render: (h) => h(App),
}).$mount("#app");

上段代码中我们就直接调用了Vue提供的directive方法来注册全局的自定义指令,该方法接收两个参数:指令名称、包含指令钩子函数的对象。

指令注册完毕后,我们就可以在项目中任意组件中的元素上使用“v-指令名称”的形式使用指令了。

需要注意的是,指令钩子函数不是必须的,大家可以把它与vue的生命周期钩子函数做类比,它们的作用就是用来让指令在不同的过程中做不同的事情。

3.2 局部注册

通常来说,如果自定义指令不是每个组件都会用到的话,我们一般局注册自定义指令就好了。

我们改造一下APP.vue文件,在其内部注册自定义指令,代码如下:

<script>
export default {
  name: "App",
  components: {},
  directives: {
    resize: {
      bind() {},
      inserted() {},
      update() {},
      componentUpdated() {},
      unbind() {},
    },
  },
};
</script>

如上所示,Vue提供了一个directives选项供我们注册自定义指令,它与data、methods同级别,上段代码中我们注册了一个名叫resize的自定义指令,该指令只允许在组件内部使用。

注意:全局注册指令使用的是directive,局部注册指令使用的是directives,很好理解,局部指令一次性注意注册很多个,全局指令依次只能注册一个。

4. 自定义指令参数详解

上一节简单介绍了局部注册自定义指令和全局注册自定义指令,可以看到指令里面有几个钩子函数,我们的操作逻辑主要在这几个钩子函数当中,所以我们有必要介绍下这几个钩子函数。

4.1 钩子函数介绍

bind:

只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted:

被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

update:

所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新

componentUpdated:

指令所在组件的 VNode及其子 VNode全部更新后调用。

unbind:

只调用一次,指令与元素解绑时调用。

上面5个就是自定义指令的全部钩子函数,每个钩子函数都是可选的,视情况而定。大家可以简单理解钩子函数顺序:指令绑定到元素时(bind)、元素插入时(inserted)、组件更新时(update)、组件更新后(componentUpdated)、指令与元素解绑时(unbind)。这些和组件的生命周期函数有点类似。

4.2 钩子函数参数介绍

为了方便我们的逻辑操作,每个钩子函数都会接收参数,我们可以用这些参数做我们想做的事。

el:

指令所绑定的元素,可以用来直接操作 DOM。

binding:

一个对象,包含以下属性:

  • name:指令名,不包括 v- 前缀。
  • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
  • 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 编译生成的虚拟节点。

oldVnode:

上一个虚拟节点,仅在updatecomponentUpdated钩子中可用。

在使用的时候,el和binding参数是我们使用得最平凡的,有了这些参数,我们的操作就变得简单起来。

5. 简单案列实战

上面两节介绍了如何注册自定义指令以及相关参数,接下来就该实战了,我们在APPVue中定义一个自定义指令,先来验证一下钩子函数的执行情况。

代码如下:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <div v-resize></div>
  </div>
</template>

<script>
export default {
  name: "App",
  components: {},
  directives: {
    resize: {
      bind() {
        console.log("bind")
      },
      inserted() {
        console.log("inserted")
      },
      update() {
        console.log("update")
      },
      componentUpdated() {
        console.log("componentUpdated")
      },
      unbind() {
        console.log("unbind")
      },
    },
  },
};
</script>

效果如下:

上面代码中我们将自定义指令resize绑定到了div元素上面,当我们刷新页面时,执行了自定义指令中的bind和inserted钩子函数,其余函数均要元素有更新才会执行。

5.1 实现v-resize指令

需求背景:

在做数据大屏或者自适应开发的时候,我们通常需要根据浏览器窗口大小的变化重新渲染页面,比如重新绘制echarts图表等功能。

需求描述:

实现自定义指令v-resize指令,窗口大小发生变化时,实时打印最新的窗口宽高。

代码实现:

// src/APP.vue
<template>
  <div id="app">
    <h1>窗口宽度:{{ innerWidth }}</h1>
    <h1>窗口高度:{{ innerHeight }}</h1>
    <div style="height: 300px; width: 80%; background: blue" v-resize></div>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      innerHeight: window.innerHeight,
      innerWidth: window.innerWidth,
    };
  },
  components: {},
  directives: {
    resize: {
      bind() {
        console.log("bind");
      },
      inserted(el, binding, vnode) {
        console.log("inserted");
        console.log(el, binding);
        let that = vnode.context;
        // 监听浏览器的resize事件
        window.addEventListener("resize", () => {
          that.innerHeight = window.innerHeight;
          that.innerWidth = window.innerWidth;
        });
      },
      update() {
        console.log("VNode更新了");
      },
      componentUpdated() {
        console.log("componentUpdated");
      },
      unbind() {
        console.log("unbind");
        window.removeEventListener("resize");
      },
    },
  },
};
</script>

效果如下:

当我们更改浏览器窗口大小时,页面上会实时打印出最新的高度和宽度,当然这儿只是一个最简单的案例,实际项目中我们通常会在窗口大小发生变化后去调用自定义的一些方法。

5.2 指令传参和传值

我们使用v-bind、v-model等内置指令时,都是可以传参和传值的,我们自定义指令也一样。

示例代码:

<template>
  <div id="app">
    <h1>窗口宽度:{{ innerWidth }}</h1>
    <h1>窗口高度:{{ innerHeight }}</h1>
    <div
      style="height: 300px; width: 80%; background: blue"
      v-resize:[args]="value"
    ></div>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      innerHeight: window.innerHeight,
      innerWidth: window.innerWidth,
      args: "我是参数",
      value: "我是传的值",
    };
  },
  components: {},
  directives: {
    resize: {
      bind(el, binding) {
        console.log("bind");
        console.log("值", binding.value);
        console.log("参数", binding.arg);
      },
    },
  },
};
</script>

效果如下:

args和value就是我们传给指令的参数和值,需要注意的是value接收变量或者表达式,arg接收字符串或者变量,具体解释可以参上参数详解那一节。

5.3 指令简写

很多时候我们不需要用到自定义指令中的所有钩子函数,常用的就那么几个,所以官方给我们提供了一种简写的方式。

代码如下:

resize(el, binding) {
   console.log("我是简写的自定义指令", binding.value);
},

上面代码的写法让我们的指令变得很简洁,上段代码的意思就是把bind和update钩子函数合二为一了,通常我们想要这两个钩子函数做同样的事的时候使用。

6. 自定义指令使用场景

知道了自定义指令如何使用,我们可以扩充一下使用场景,加深一下大家对自定义指令的理解。

6.1 v-copy

实现一键复制文本内容,用于鼠标右键粘贴。

6.2 v-longpress

实现长按,用户需要按下并按住按钮几秒钟,触发相应的事件。

6.3 v-debounce

防止按钮在短时间内被多次点击,使用防抖函数限制规定时间内只能点击一次。

6.4 v-LazyLoad

实现一个图片懒加载指令,只加载浏览器可见区域的图片。

6.5 v-draggable

实现一个拖拽指令,可在页面可视区域任意拖拽元素。

上面的一些自定义指令都是需求中非常常见的,对应的指令代码网上也有很多,但是我建议大家自己下去实践一下。

总结

自定义指令的语法规则和用法很简单,复杂的是我们要用来解决什么问题。在合适的场景下使用合适的解决办法才是最重要的。