本篇文章,我们讲的是Vue
中,自定义指令的内部实现方式。具体用法官方文档已经解释的很清楚了。
自定义指令,同样有全局和局部两种方式。
全局指令的定义:
Vue.directive('demo', {
bind: function(){
...
}
})
全局指令的实现方式,和全局组件、全局过滤器一致。
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
...
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
接收两个参数,第一个是指令名,第二个参数是一个函数或一个对象,如果是函数,则会新建一个对象,并把definition
赋值给bind
和update
属性。最终,会把指令的定义放在Vue.options.directives
上。在实例化组件对象时,会合并到vm.$options
上。
局部指令的定义:
new Vue({
directives: {
demo: {
bind: function(){
...
}
}
}
})
其实同全局指令类似,demo
的值也可以是一个函数,在合并配置项时,会新建一个对象,并把赋值给bind
和update
属性。
自定义指令的用法和内置指令的用法一致,通过v-指令名
的方式添加到模板的标签上,然后在对应的钩子函数中,执行相应的操作。
模板的解析流程在指令概述中简单概括了一下。这里我们详细的说一下与自定义指令密切相关的内容。
首先还是指令的解析:
modifiers = parseModifiers(name)
if (modifiers) {
// const modifierRE = /\.[^.]+/g
name = name.replace(modifierRE, '')
}
...
// const dirRE = /^v-|^@|^:/
name = name.replace(dirRE, '')
// parse arg
// const argRE = /:(.*)$/
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)
假如我们的指令时这样的v-demo:foo.a.b = 'demo_value'
,其中data_demo
绑定的数据为test
。初始情况下name
和rawName
都是``v-demo:foo.a.b,
modifierRE`会去掉修饰符,`dirRE`是去掉前面的`v-`,`argRE`匹配冒号及后面的值,最终`arg`为`foo`,`name`为`demo`,`value`是`demo_value`,`modifiers`为`{a: true, b: true}`。
genDirectives
内生成添加到directives
属性中的对象的生成过程如下:
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)}` : ''
}},`
该对象最终会传到我们自己定义的bind
、update
等函数的第二个参数。每一项的值分别如下:
{
name: 'demo',
rawName: 'v-demo:foo.a.b',
value: 'test',
expression: 'demo_value',
arg: 'foo',
modifiers: {a: true, b: true}
}
概述中我们也提到过,在组件patch
的不同阶段,就会修改data
数据的钩子函数,进而在_update
函数中调用我们定义的各个钩子函数来实现指令的操作。
function _update (oldVnode, vnode) {
// 第一次实例化组件时,oldVnode是emptyNode
const isCreate = oldVnode === emptyNode
// 销毁组件时,vnode是emptyNode
const isDestroy = vnode === emptyNode
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) {
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) {
// new directive, bind
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
// existing directive, update
dir.oldValue = oldDir.value
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir)
}
}
}
if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', callInsert)
} else {
callInsert()
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), '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
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}
normalizeDirectives
方法是从组件的vm.$options.directives
中获取指令的定义,emptyModifiers
是一个空对象,最终res
的键值是我们上面提到的rawName
格式,resolveAsset
方法就是获取指令的定义,同之前讲到的获取子组件一样,会转为驼峰、中划线的各种形式来尝试获取。指令的定义,会添加到我们之前生成的对象的def
属性中。
function normalizeDirectives (
dirs: ?Array<VNodeDirective>,
vm: Component
): { [key: string]: VNodeDirective } {
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
}
接下来循环新vnode
上绑定的指令,如果第一次绑定,则直接调用bind
钩子函数。调用函数使用的是callHook
函数,在调用时会传入vnode.elm
, dir
,vnode
,oldVnode
, isDestroy
五个参数,与文档中提到的钩子函数的参数正好对应。若同时还添加了inserted
钩子,则会先把它添加到dirsWithInsert
数组中。
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
const fn = dir.def && dir.def[hook]
if (fn) {
fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
}
}
如果不是第一次绑定,则调用update
钩子函数,若同时定义了componentUpdated
钩子,则会先把它添加到dirsWithPostpatch
数组中。
接着,如果是vnode
是第一次创建,则会把dirsWithInsert
数组中的回调追加到vnode.data.hook.insert
中执行。这是因为vnode.data.hook.insert
调用的时机,是在dom
插入到页面之后,这个时候可以真正操作dom
。否则,比如diff
操作复用了之前的元素时,因为元素已经在页面中,此时则直接调用dirsWithInsert
数组中的钩子函数。
dirsWithPostpatch
的操作与之前类似,不过这次是添加到vnode.data.hook.postpatch
钩子函数中,该钩子函数,是在patch
之后调用。
最后,如果不是第一次创建,就调用旧vnode
中新vnode
不存在的指令的unbind
钩子函数。
以上,就是Vue
中,对于自定义指令的全部处理过程。