在 Vue 中,组件负责"可见"的 UI,自定义指令则负责"不可见"的底层 DOM 行为。当你需要直接操作节点、监听第三方库、或封装浏览器原生 API 时,指令往往比组件更轻量、更灵活。Vue 为自定义指令设计了五条钩子函数,覆盖从首次绑定到最终解绑的完整生存周期。
一、bind:指令的"构造函数"
当指令第一次绑定到元素时触发,且只触发一次。
此时父节点可能尚未挂载到 DOM,因此不要在这里做需要获取父元素尺寸、位置的操作。
典型用途
- 注册全局事件(如
window.resize
) - 初始化第三方实例(如
new Sortable(el)
) - 设置一次性样式或属性
js
bind(el, binding) {
el.__onResize = () => { /* ... */ }
window.addEventListener('resize', el.__onResize)
}
二、inserted:元素真正"落位"
当元素已经被插入父节点(哪怕父节点仍在内存中,尚未插入 document
)时触发。
此时可以安全地读取 offsetParent
、getBoundingClientRect
等信息。
典型用途
- 启动需要 DOM 位置的动画
- 实例化依赖父级布局的库(如
Popper.js
) - 聚焦输入框
js
inserted(el) {
el.focus()
}
三、update:数据变了,视图还没 patch 完
每当组件的 VNode 重新渲染时触发,不管绑定值是否真的改变。
如果你只想在值变化时执行逻辑,需要手动比较 binding.value
与 binding.oldValue
。
典型用途
- 根据新值更新样式或文本
- 节流/防抖高频 DOM 操作
js
update(el, binding) {
if (binding.value !== binding.oldValue) {
el.style.color = binding.value
}
}
四、componentUpdated:整个组件已经 patch 完成
与 update
不同,此钩子在当前组件及其所有子组件的 DOM 更新周期结束后触发。
适合需要等待子节点渲染完毕再执行的场景。
典型用途
- 重新计算滚动容器高度
- 在子列表渲染后刷新第三方滚动条
js
componentUpdated(el) {
el.scrollTop = el.scrollHeight
}
五、unbind:指令的"析构函数"
当指令与元素解绑(元素被 v-if
移除、路由切换、手动销毁)时触发,且只触发一次。
务必在此清理所有副作用,防止内存泄漏。
典型用途
- 移除全局事件监听
- 销毁第三方实例
- 清除定时器
js
unbind(el) {
window.removeEventListener('resize', el.__onResize)
el.__onResize = null
}
六、钩子参数速览
所有钩子都会收到统一的四个参数,按出场频率排序:
-
el
绑定到的真实 DOM 节点,可直接操作。
-
binding
一个对象,携带所有绑定信息:
name
:指令名(不含v-
)value
:当前绑定值oldValue
:上一次绑定值(仅在update
和componentUpdated
中)expression
:字符串形式的表达式arg
:指令参数,如v-color:red
中的red
modifiers
:修饰符对象,如{ prevent: true }
-
vnode
Vue 生成的虚拟节点,可访问
context
、key
等元数据。 -
oldVnode
更新前的旧虚拟节点,仅在
update
和componentUpdated
中可用。
实战示例:一个"可关闭提示"指令
js
export default {
bind(el, { value }) {
el.style.position = 'relative'
el.innerHTML += '<span class="close">×</span>'
el.__close = el.querySelector('.close')
el.__handler = () => el.remove()
el.__close.addEventListener('click', el.__handler)
},
unbind(el) {
el.__close.removeEventListener('click', el.__handler)
}
}