Vue自定义指令也分全局指令和局部指令两种,全局指令和局部指令的使用方法分别为:
// 全局指令使用方法
Vue.directive('test', {
bind: function(){
...
}
})
// 局部指令使用方法
Vue({
directives: {
test: {
bind: function(){
...
}
}
}
})
上面写的bind
方法其实就是自定义指令的几个钩子函数之一,可以视情况添加其他的钩子函数,官方的介绍:
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中),实际应用场景:被绑定元素插入父节点后,做一些初始化的操作,比如改变颜色,字体大小等等
update
:所在组件的VNode
更新时调用,但是可能发生在其子 VNode
更新之前,实际应用场景:被绑定元素状态/样式、内容发生改变时触发
componentUpdated
:指令所在组件的 VNode
及其子VNode
全部更新后调用
unbind
:只调用一次,指令与元素解绑时调用,实际应用场景:当指令绑定的元素从 dom 中删除时触发,可在这个时候做一些需要的操作
指令钩子函数会被传入以下参数:
el
:指令所绑定的元素,可以用来直接操作 DOM 。
binding
:一个对象,包含以下属性:
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 编译生成的虚拟节点
oldVnode
:上一个虚拟节点,仅在 update
和 componentUpdated
钩子中可用。
在文件src/core/global-api/index.js
中可以看到initAssetRegisters
,这个方法会用来初始化component
以及directive
export function initGlobalAPI (Vue: GlobalAPI) {
.
.
.
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
initGlobalAPI
会接受两个参数,第一个参数代表指令的名字,第二个参数如果是函数,则会创建一个
{ bind: definition, update: definition}
对象,并将他赋值给definition
,这个对象会在生成Vue
实例的时候会合并到vm.$options.directives
上。顺便说一下这里面也对自定义component
进行一个处理,他会根据传入的组件定义创建一个Vue
子类,设置好name
或者id
,最后返回。
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
指令的解析在src/compiler/parser/index.js
中完成,用正则匹配指令,在解析完v-bind
、v-on
等等原生的指令后,调用addDirective
,将指令属性添加到当前节点的directives
上。其中的正则modifierRE
会去掉修饰符,dirRE
是去掉前面的v-
,argRE
匹配冒号及后面的值
else { // normal directives
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
const arg = argMatch && argMatch[1]
if (arg) {
name = name.slice(0, -(arg.length + 1))
}
addDirective(el, name, rawName, value, arg, modifiers, list[i])
if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value)
}
}
export function addDirective (
el: ASTElement,
name: string,
rawName: string,
value: string,
arg: ?string,
modifiers: ?ASTModifiers,
range?: Range
) {
(el.directives || (el.directives = [])).push(rangeSetItem({ name, rawName, value, arg, modifiers }, range))
el.plain = false
}
编译阶段,在src/compiler/codegen/index.js
中,调用genDirectives
,循环遍历directives
对象,最终生成对应的属性,生成的形式为
directives:[{
name: '',
rawName: '',
value: '',
expression: '',
arg: '',
modifiers: {}
}
...
]
其代码为
...
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:"${dir.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
...
最后,会在src/core/vdom/modules/directives.js
中调用_update
,这个方法的主要作用遍历指令对象,将上面的属性通过回调函数调用对应的钩子函数,然后处理,钩子函数即上面提到的bind
、update
、inserted
等
function _update (oldVnode, vnode) {
// 第一次实例化组件时,oldVnode是emptyNode
const isCreate = oldVnode === emptyNode
// 销毁组件时,vnode是emptyNode
const isDestroy = vnode === emptyNode
//normalizeDirectives函数是从组件的vm.$options.directives中获取指令的定义
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
const dirsWithInsert = []
const dirsWithPostpatch = []
let key, oldDir, dir
for (key in newDirs) {
//循环新vnode上绑定的指令
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) {
// new directive, bind => 如果第一次绑定,则直接调用bind钩子函数
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
//若同时还添加了inserted钩子,则会先把它添加到dirsWithInsert数组中。
dirsWithInsert.push(dir)
}
} else {
// existing directive, update => 如果不是第一次绑定,则调用update钩子函数
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
//若同时定义了componentUpdated钩子,则会先把它添加到dirsWithPostpatch数组中。
dirsWithPostpatch.push(dir)
}
}
}
if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
//如果是vnode是第一次创建,
//则会把dirsWithInsert数组中的回调追加到vnode.data.hook.insert中执行
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
// 如果不是第一次创建,就调用旧vnode中新vnode不存在的指令的unbind钩子函数
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}