最近经常使用指令来实现一些功能,觉得甚是好用。然而却遇到一个问题:v-if和我自定义指令在同一个标签里使用的时候,我自定义的指令传的值通过bind.value没获取到值。后来换成v-show是我期待的结果,但是当我把v-if换成v-else(v-if 和 v-else指令顺序颠倒),也能获取到binding.value的值。这就让我对指令很感兴趣,想知道vue内部又是怎么去实现指令的。
入口文件
指令在官方文档中属于全局API,所以从src\core\global-api\index.js中查找指令。
.....
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue) //初始化指令
......
于是我查看了文件src\core\global-api\assets.js,发现了这段代码:
ASSET_TYPES.forEach(type => {
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
}
}
})
该函数的参数有两个,其中id就是指令的名字,definition就是我们平时自定义指令传的对象或者函数。这段代码的意思也就很清楚了:如果是指令,且第二个参数是函数,则会新定义一个对象,并把definition赋值给bind和update属性。最后,会把指令的定义赋给Vue.options.directives[指令名]。感兴趣的可以自己再vue官网打印一下。
上面是全局API的实现方式。但是其实指令是加在模板的标签上的,所以模板编译部分也做了处理。
模板编译
模板编译的入口文件src\compiler\parser\index.js,你会很容易看到有个函数parseHTML(template, {}),代码如下:
parseHTML(template, {
//......省略代码
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element) //解析v-for
processIf(element) //解析v-if
processOnce(element) //解析v-once
}
//......省略
if (!unary) {
currentParent = element
stack.push(element)
} else {
//自定义指令走这里
closeElement(element)
}
//......省略
})
一步一步向下找,会找到函数processElement(),进而发现除了上述代码中直接解析的指令,剩下的指令和属性统一都交给了processAttrs()函数处理,比如v-bind、v-on以及普通属性等。截取部分该函数的代码:
//省略部分代码
else { // normal directives
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
在该函数中处理了要传给addDirective的参数对应的值,最终在addDirective函数中把自定义指令添加到el上。然后我们在文件src\compiler\codegen\index.js中会找到constructor里的代码:
constructor (options: CompilerOptions) {
this.options = options
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
this.directives = extend(extend({}, baseDirectives), options.directives)
//省略代码
}
可以继续在该文件中找到genDirectives函数,可以看到添加到directives属性中的对象的生成过程如下:
function genDirectives (el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives
//省略代码
if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
对于添加到directives上的数据,则是在patch中通过添加到cbs中的钩子函数处理的。具体过程见src/core/vdom/modules/directives.js文件中。
export default {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
可以看到,这三个钩子函数,最终调用的都是updateDirectives方法。
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode)
}
}
如果updateDirectives()函数接受到的两个参数data中有directives属性,则调用_update方法来进行处理。_update方法的主要操作,其实就是调用指令的各种钩子函数。
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)
}
}
}
}
指令使用
Vue.directive('my-directive', {
bind: function () {},
inserted: function () {},
update: function () {},
componentUpdated: function () {},
unbind: function () {}
})
上面的_update 函数对应着这几个钩子函数,其中:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
指令钩子函数会被传入以下参数:
-
el
:指令所绑定的元素,可以用来直接操作 DOM 。 -
binding
:一个对象,包含以下属性: -
vnode
:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。 -
oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
看完源代码,在回过头来看官网注释,会发现自己对官网的说明印象更加深刻,也更加深自己的理解。
答案
关于开头提到的问题的答案:其实还是和v-if有关,v-if在编译过程中会被转化成三元表达式,条件不满足的时候,是不会渲染此节点的,自然值也没拿到了。
谢谢阅读,有问题可以互相交流哦
网友评论