指令参数:el、binding、vnode、oldVnode 四个参数详解
自定义指令的钩子函数会收到四个参数,分别是 el、binding、vnode 和 oldVnode。很多初学者看到这四个参数就头大,觉得太抽象。其实你想想看,它们各自扮演的角色非常清晰。
我个人习惯把这四个参数分成两类:DOM 相关和虚拟 DOM 相关。el 是实实在在的 DOM 元素,binding 是绑定的配置信息,vnode 和 oldVnode 则是 Vue 内部的虚拟节点。
el:真实的 DOM 元素
el 就是指令所绑定的那个 DOM 元素。它是原生的,不是 Vue 包装过的。你可以直接对它进行各种 DOM 操作。
核心要点:el 就是 document.querySelector 拿到的那个东西,完全一样。
// 举个例子
Vue.directive('demo', {
bind(el) {
// el 就是 <div v-demo> 这个 div
el.style.color = 'red'
el.addEventListener('click', () => {
console.log('我被点击了')
})
}
})
我在项目中遇到过一个问题:有人试图在 el 上使用 Vue 的响应式数据,比如 el.someData = this.someValue。这其实是不对的,el 不是响应式的,你赋值了也不会触发视图更新。要操作数据,得通过 binding 或者组件实例。
注意:el 是真实的 DOM,不是虚拟 DOM。不要试图在 el 上挂载响应式数据,它不会自动更新。
binding:指令的配置信息
这个参数最常用,也最复杂。它是一个对象,包含了指令的所有配置信息。说白了,你写在指令后面的所有东西,都会解析到这个对象里。
binding 对象包含以下属性:
| 属性名 | 类型 | 说明 |
|---|---|---|
name |
String | 指令名,不包括 v- 前缀 |
value |
Any | 指令的绑定值,比如 v-demo="1 + 1" 的值是 2 |
oldValue |
Any | 上一次的绑定值,仅在 update 和 componentUpdated 中可用 |
expression |
String | 绑定值的字符串形式,比如 "1 + 1" |
arg |
String | 传给指令的参数,比如 v-demo:foo 的参数是 "foo" |
modifiers |
Object | 修饰符对象,比如 v-demo.prevent 的 modifiers 是 { prevent: true } |
小技巧:我个人习惯在开发时用 console.log(binding) 打印出来看看,一目了然。比看文档快多了。
// 假设模板中这样使用
// <div v-demo:foo.prevent="count + 1"></div>
Vue.directive('demo', {
bind(el, binding) {
console.log(binding.name) // 'demo'
console.log(binding.value) // count + 1 的计算结果
console.log(binding.expression) // 'count + 1'
console.log(binding.arg) // 'foo'
console.log(binding.modifiers) // { prevent: true }
}
})
我曾经踩过一个坑:在 bind 钩子里访问 binding.oldValue,结果发现它是 undefined。后来才意识到,bind 是第一次绑定,哪来的旧值?oldValue 只在 update 和 componentUpdated 里才有意义。
vnode:虚拟节点
vnode 是 Vue 的虚拟节点,它包含了组件或元素的所有信息。比如标签名、属性、子节点、组件实例等等。
说实话,日常开发中用到 vnode 的场景不多。但如果你要做一些高级操作,比如访问组件实例,那就离不开它了。
Vue.directive('get-instance', {
bind(el, binding, vnode) {
// 获取组件实例
const vm = vnode.context
console.log(vm.$options.name) // 组件名称
// 如果是组件节点,还可以拿到组件实例
if (vnode.componentInstance) {
console.log(vnode.componentInstance.someData)
}
}
})
重点:vnode.context 就是当前组件的实例,通过它可以访问组件的数据和方法。但要注意,这破坏了组件的封装性,能不用尽量别用。
oldVnode:上一次的虚拟节点
oldVnode 只在 update 和 componentUpdated 钩子里有值。它代表指令更新前的虚拟节点状态。
你想想看,为什么需要这个参数?因为有时候你需要对比新旧状态,来决定要不要做某些操作。比如一个拖拽指令,你需要知道元素之前的位置和现在的位置,才能计算偏移量。
Vue.directive('track-change', {
update(el, binding, vnode, oldVnode) {
// 对比新旧值
if (binding.value !== binding.oldValue) {
console.log('值发生了变化')
console.log('旧值:', binding.oldValue)
console.log('新值:', binding.value)
}
// 对比虚拟节点
if (oldVnode && oldVnode.tag !== vnode.tag) {
console.log('元素类型变了')
}
}
})
注意:在 bind 钩子里 oldVnode 是 undefined,别指望它。只有在 update 和 componentUpdated 里才能用。
实际应用:一个完整的例子
说了这么多理论,不如看个实际例子。这是我之前封装的一个权限指令,用到了上面提到的所有参数:
Vue.directive('permission', {
bind(el, binding, vnode) {
const { value, arg } = binding
const permissions = vnode.context.$store.state.user.permissions
// arg 是权限类型,比如 'read', 'write'
// value 是权限标识,比如 'article'
if (!permissions[arg] || !permissions[arg].includes(value)) {
el.parentNode && el.parentNode.removeChild(el)
}
},
update(el, binding, vnode, oldVnode) {
// 如果权限列表变了,重新检查
const { value, arg } = binding
const permissions = vnode.context.$store.state.user.permissions
if (!permissions[arg] || !permissions[arg].includes(value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
// 使用方式
// <button v-permission:write="'article'">编辑文章</button>
这个例子中,arg 用来区分权限类型,value 是具体的权限标识,vnode.context 用来获取 Vuex 中的权限数据。嗯,这样设计是不是很清晰?
我的建议:刚开始学自定义指令时,别急着把所有参数都用上。先从 el 和 binding 入手,等用熟了再考虑 vnode 和 oldVnode。循序渐进,反而学得更扎实。
最后说一句,这四个参数虽然看起来多,但每个都有明确的职责。el 管 DOM,binding 管配置,vnode 管虚拟节点,oldVnode 管对比。记住这个分工,你就不会搞混了。