开始之前我们先学习⼀下指令系统这个词指令系统是计算机硬件的语⾔系统,也叫机器语⾔,它是系统程序员看到的计算机的主要属性。因此指令系统表征了计算机的基本功能决定了机器所要求的能⼒。

在 vue 中提供了⼀套为数据驱动视图更为⽅便的操作,这些操作被称为指令系统,我们看到的v-开头的⾏内属性,都是指令,不同的指令可以完成或实现不同的功能除了核⼼功能默认内置的指令(v-modelv-show),Vue也允许注册⾃定义指令指令使⽤的⼏种⽅式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//会实例化⼀个指令,但这个指令没有参数
v-xxx

// -- 将值传到指令中
v-xxx="value"

// -- 将字符串传⼊到指令中,如`v-html="'<p>内容</p>'"`
v-xxx="'string'"

// -- 传参数(`arg`),如`v-bind:class="className"`
v-xxx:arg="value"

// -- 使⽤修饰符(`modifier`)
v-xxx:arg.modifier="value"

如何实现

注册⼀个⾃定义指令有全局注册与局部注册,全局注册主要是通过Vue.directive⽅法进⾏注册,Vue.directive第⼀个参数是指令的名字(不需要写上v-前缀),第⼆个参数可以是对象数据,也可以是⼀个指令函数.

1
2
3
4
5
6
7
8
// 注册⼀个全局⾃定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插⼊到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus() // ⻚⾯加载完成之后⾃动让输⼊框获取到焦点的⼩功能
},
})

局部注册通过在组件options选项中设置directive属性

1
2
3
4
5
6
7
8
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus() // ⻚⾯加载完成之后⾃动让输⼊框获取到焦点的⼩功能
}
}
}

然后你可以在模板中任何元素上使⽤新的v-focusproperty,如下:

1
<input v-focus />

⾃定义指令也像组件那样存在钩⼦函数:

  • bind :只调⽤⼀次,指令第⼀次绑定到元素时调⽤。在这⾥可以进⾏⼀次性的初始化设置
  • inserted :被绑定元素插⼊⽗节点时调⽤ (仅保证⽗节点存在,但不⼀定已被插⼊⽂档中)
  • update :所在组件的VNode更新时调⽤,但是可能发⽣在其⼦VNode更新之前。指令的值可能发⽣了改变,也可能没有。但是你可以通过⽐较更新前后的值来忽略不必要的模板更新
  • componentUpdated :指令所在组件的VNode及其⼦VNode全部更新后调⽤
  • unbind :只调⽤⼀次,指令与元素解绑时调⽤

所有的钩⼦函数的参数都有以下:

  • el :指令所绑定的元素,可以⽤来直接操作DOM
  • binding :⼀个对象,包含以下property
  • 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之外,其它参数都应该是只读的,切勿进⾏修改。如果需要在钩⼦之间共享数据,建议通过元素的dataset来进⾏

举个例⼦:

1
2
3
4
5
6
7
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
<script>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // "white"
console.log(binding.value.text) // "hello!"
})
</script>

应⽤场景

使⽤⾃定义指令可以满⾜我们⽇常⼀些场景,这⾥给出⼏个⾃定义指令的案例:

  • 表单防⽌重复提交
  • 图⽚懒加载
  • ⼀键 Copy 的功能

表单防⽌重复提交

表单防⽌重复提交这种情况设置⼀个v-throttle⾃定义指令来实现

举个例⼦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.设置v-throttle⾃定义指令
Vue.directive('throttle', {
bind: (el, binding) => {
let throttleTime = binding.value; // 节流时间
if (!throttleTime) { // ⽤户若不设置节流时间,则默认2s
throttleTime = 2000;
}
let cbFun;
el.addEventListener('click', event => {
if (!cbFun) { // 第⼀次执⾏
cbFun = setTimeout(() => {
cbFun = null;
}, throttleTime);
} else {
event && event.stopImmediatePropagation();
}
}, true);
},
});
// 2.为button标签设置v-throttle⾃定义指令
<button @click="sayHello" v-throttle>提交</button>

图⽚懒加载

设置⼀个v-lazy⾃定义指令完成图⽚懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const LazyLoad = {
// install⽅法
install(Vue, options) {
// 代替图⽚的loading图
let defaultSrc = options.default;
Vue.directive('lazy', {
bind(el, binding) {
LazyLoad.init(el, binding.value, defaultSrc);
},
inserted(el) {
// 兼容处理
if ('IntersectionObserver' in window) {
LazyLoad.observe(el);
} else {
LazyLoad.listenerScroll(el);
}

},
})
},
// 初始化
init(el, val, def) {
// data-src 储存真实src
el.setAttribute('data-src', val);
// 设置src为loading图
el.setAttribute('src', def);
},
// 利⽤IntersectionObserver监听el
observe(el) {
let io = new IntersectionObserver(entries => {
let realSrc = el.dataset.src;
if (entries[0].isIntersecting) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute('data-src');
}
}
});
io.observe(el);
},
// 监听scroll事件
listenerScroll(el) {
let handler = LazyLoad.throttle(LazyLoad.load, 300);
LazyLoad.load(el);
window.addEventListener('scroll', () => {
handler(el);
});
},
// 加载真实图⽚
load(el) {
let windowHeight = document.documentElement.clientHeight
let elTop = el.getBoundingClientRect().top;
let elBtm = el.getBoundingClientRect().bottom;
let realSrc = el.dataset.src;
if (elTop - windowHeight < 0 && elBtm > 0) {
if (realSrc) {
el.src = realSrc;
el.removeAttribute('data-src');
}
}
},
// 节流
throttle(fn, delay) {
let timer;
let prevTime;
return function(...args) {
let currTime = Date.now();
let context = this;
if (!prevTime) prevTime = currTime;
clearTimeout(timer);

if (currTime - prevTime > delay) {
prevTime = currTime;
fn.apply(context, args);
clearTimeout(timer);
return;
}
timer = setTimeout(function() {
prevTime = Date.now();
timer = null;
fn.apply(context, args);
}, delay);
}
}
}
export default LazyLoad;

⼀键Copy的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { Message } from 'ant-design-vue';
const vCopy = { //
/*
bind 钩⼦函数,第⼀次绑定时调⽤,可以在这⾥做初始化设置
el: 作⽤的 dom 对象
value: 传给指令的值,也就是我们要 copy 的值
*/
bind(el, { value }) {
el.$value = value; // ⽤⼀个全局属性来存传进来的值,因为这个值在别的钩⼦函数⾥
还会⽤ 到
el.handler = () => {
if (!el.$value) {
// 值为空的时候,给出提示,我这⾥的提示是⽤的 ant-design-vue 的提示,你们随意
Message.warning('⽆复制内容');
return;
}
// 动态创建 textarea 标签
const textarea = document.createElement('textarea');
// 将该 textarea 设为 readonly 防⽌ iOS 下⾃动唤起键盘,同时将 textarea 移
出可视区域
textarea.readOnly = 'readonly';
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插⼊到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
// textarea.setSelectionRange(0, textarea.value.length);
const result = document.execCommand('Copy');
if (result) {
Message.success('复制成功');
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的⼀键 copy 啦
el.addEventListener('click', el.handler);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener('click', el.handler);
},
};
export default vCopy;

关于⾃定义指令还有很多应⽤场景,如:拖拽指令、⻚⾯⽔印、权限校验等等应⽤场景.