用法

全局: Vue.directive —-> 存放在 Vue.options['directives']
局部: 组件内的 directive 选项中定义 —-> 存放在 vm.$options['directives']

1
2
3
4
5
6
7
8
9
<input v-focus></input>;
// 注册一个全局自定义指令 `v-focus`
Vue.directive("focus", {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus();
},
});

监听时机

钩子触发时机

init

  • 触发时机: 已创建 VNode,在 patch 期间发现新的虚拟节点时被触发
  • 回调参数: VNode

create

  • 触发时机: 已基于 VNode 创建了 DOM 元素
  • 回调参数: emptyNode 和 VNode

activate

  • 触发时机: keep-alive 组件被创建
  • 回调参数: emptyNode 和 VNode

insert

  • 触发时机: VNode 对应的 DOM 元素被插入到父节点中时被触发
  • 回调参数: VNode

prepatch

  • 触发时机: 一个 VNode 即将被 patch 前触发
  • 回调参数: oldVNode 和 VNode

update

  • 触发时机: 一个 VNode 更新时触发
  • 回调参数: oldVNode 和 VNode

postpatch

  • 触发时机: 一个 VNode 被 patch 完毕时触发
  • 回调参数: oldVNode 和 VNode

destory

  • 触发时机: 一个 VNode 对应的 DOM 元素从 DOM 中移除时或者它的父元素从 DOM 中移除时触发
  • 回调参数: VNode

remove

  • 触发时机: 一个 VNode 对应的 DOM 元素从 DOM 中移除时触发。与 destory 不同的是,如果是直接将该 VNode 的父元素从 DOM 中移除导致该元素被移除,那么不会触发
  • 回调参数: VNode 和 removeCallback

在 create、update、destory 三个阶段监听自定义指令

指令钩子函数

Vue 对于自定义指令定义对象提供了几个钩子函数,这几个钩子函数分别对应着指令的几种状态,一个指令从第一次被绑定到元素上到最终与被绑定的元素解绑,它会经过以下几种状态

  • bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  • inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
  • update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
  • componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  • unbind: 只调用一次,指令与元素解绑时调用

updateDirectives: 在合适的时机执行定义指令时所设置的钩子函数

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
function updateDirectives(oldVnode, vnode) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode);
}
}

function _update(oldVnode, vnode) {
const isCreate = oldVnode === emptyNode; // 判断当前节点vnode对应的旧节点oldVnode是不是一个空节点,如果是的话,表明当前节点是一个新创建的节点
const isDestroy = vnode === emptyNode; // 判断当前节点vnode是不是一个空节点,如果是的话,表明当前节点对应的旧节点将要被销毁
const oldDirs = normalizeDirectives(
oldVnode.data.directives,
oldVnode.context
); // 旧的指令集合,即oldVnode中保存的指令
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context); // 新的指令集合,即vnode中保存的指令

const dirsWithInsert = []; // 保存需要触发inserted指令钩子函数的指令列表
const dirsWithPostpatch = []; // 保存需要触发componentUpdated指令钩子函数的指令列表

let key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// 判断当前循环到的指令名key在旧的指令列表oldDirs中是否存在,如果不存在,说明该指令是首次绑定到元素上的一个新指令,此时调用callHook触发指令中的bind钩子函数
callHook(dir, "bind", vnode, oldVnode);
// 判断如果该新指令在定义时设置了inserted钩子函数,那么将该指令添加到dirsWithInsert中,以保证执行完所有指令的bind钩子函数后再执行指令的inserted钩子函数
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir);
}
} else {
// 当前循环到的指令名key在旧的指令列表oldDirs中存在时,说明该指令在之前已经绑定过了,那么这一次的操作应该是更新指令
dir.oldValue = oldDir.value;
dir.oldArg = oldDir.arg;
// 保存上一次指令的value属性值和arg属性值,然后调用callHook触发指令中的update钩子函数
callHook(dir, "update", vnode, oldVnode);
// 判断如果该指令在定义时设置了componentUpdated钩子函数,那么将该指令添加到dirsWithPostpatch中,以保证让指令所在的组件的VNode及其子VNode全部更新完后再执行指令的componentUpdated钩子函数
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}

// 判断dirsWithInsert数组中是否有元素,如果有,则循环dirsWithInsert数组,依次执行每一个指令的inserted钩子函数
if (dirsWithInsert.length) {
// 并没有直接去循环执行每一个指令的inserted钩子函数,而是新创建了一个callInsert函数,当执行该函数的时候才会去循环执行每一个指令的inserted钩子函数
// ==> 1. 因为指令的inserted钩子函数必须在被绑定元素插入到父节点时调用,那么如果是一个新增的节点,如何保证它已经被插入到父节点了呢
// 2. 当DOM节点在被插入到父节点时会触发insert函数,当虚拟DOM渲染更新的insert钩子函数被调用的时候就标志着当前节点已经被插入到父节点了,所以要在虚拟DOM渲染更新的insert钩子函数内执行指令的inserted钩子函数
// 3. 当一个新创建的元素被插入到父节点中时虚拟DOM渲染更新的insert钩子函数和指令的inserted钩子函数都要被触发
// 4. 可以把这两个钩子函数通过调用mergeVNodeHook方法进行合并,然后统一在虚拟DOM渲染更新的insert钩子函数中触发,就保证了元素确实被插入到父节点中才执行的指令的inserted钩子函数
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], "inserted", vnode, oldVnode);
}
};
if (isCreate) {
mergeVNodeHook(vnode, "insert", callInsert);
} else {
callInsert();
}
}

// 需要保证指令所在的组件的VNode及其子VNode全部更新完后再执行指令的componentUpdated钩子函数,所以将虚拟DOM渲染更新的postpatch钩子函数和指令的componentUpdated钩子函数进行合并触发
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, "postpatch", () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], "componentUpdated", vnode, oldVnode);
}
});
}

// 当newDirs循环完毕后,再循环oldDirs,如果某个指令存在于旧的指令列表oldDirs而在新的指令列表newDirs中不存在,那说明该指令是被废弃的,所以则触发指令的unbind钩子函数对指令进行解绑
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook(oldDirs[key], "unbind", oldVnode, oldVnode, isDestroy);
}
}
}
}

// normalizeDirectives 用来模板中使用到的指令从存放指令的地方取出来,并将其格式进行统一化
function normalizeDirectives(dirs, vm) {
const res = Object.create(null);
if (!dirs) {
return res;
}
let i, dir;
for (i = 0; i < dirs.length; i++) {
dir = dirs[i];
if (!dir.modifiers) {
dir.modifiers = emptyModifiers;
}
res[getRawDirName(dir)] = dir;
dir.def = resolveAsset(vm.$options, "directives", dir.name, true);
}
return res;
}

// ------> 转化后结果
// {
// 'v-focus':{
// name : 'focus' , // 指令的名称
// value : '', // 指令的值
// arg:'', // 指令的参数
// modifiers:{}, // 指令的修饰符
// def:{
// inserted:fn
// }
// }
// }