钩子函数

自定义指令提供了多个钩子函数,让你可以在指令生命周期的不同阶段执行操作。理解每个钩子的调用时机是编写高质量指令的关键。

钩子函数列表

Vue.directive('my-directive', {
  bind(el, binding, vnode, oldVnode) {
    // 指令首次绑定到元素时调用
  },
  inserted(el, binding, vnode, oldVnode) {
    // 被绑定元素插入父节点时调用
  },
  update(el, binding, vnode, oldVnode) {
    // 所在组件的 VNode 更新时调用
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  },
  unbind(el, binding, vnode, oldVnode) {
    // 指令与元素解绑时调用
  }
})

bind

调用时机:指令首次绑定到元素时,只调用一次。

适用场景

  • 初始化设置
  • 添加 CSS 类
  • 设置初始属性
Vue.directive('color', {
  bind(el, binding) {
    el.style.color = binding.value
  }
})

注意:此时元素可能还未插入 DOM,父节点可能不存在。

inserted

调用时机:被绑定元素插入父节点时调用(父节点存在即可,不必已在文档中)。

适用场景

  • 操作 DOM(如 focus)
  • 获取元素尺寸
  • 添加事件监听
Vue.directive('focus', {
  inserted(el) {
    el.focus()
  }
})

为什么 focus 要在 inserted 而不是 bind?

因为 focus 需要元素在 DOM 中才能生效,bind 时元素可能还未渲染。

update

调用时机:所在组件的 VNode 更新时调用,可能发生在其子 VNode 更新之前。

适用场景

  • 响应数据变化
  • 更新元素状态
Vue.directive('color', {
  bind(el, binding) {
    el.style.color = binding.value
  },
  update(el, binding) {
    if (binding.value !== binding.oldValue) {
      el.style.color = binding.value
    }
  }
})

componentUpdated

调用时机:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

适用场景

  • 需要等待子组件更新完成
  • 依赖完整渲染结果的操作
Vue.directive('scroll-to-bottom', {
  componentUpdated(el) {
    el.scrollTop = el.scrollHeight
  }
})

unbind

调用时机:指令与元素解绑时调用,只调用一次。

适用场景

  • 清理事件监听
  • 取消定时器
  • 释放资源
Vue.directive('click-outside', {
  bind(el, binding) {
    el._clickOutside = function(event) {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el._clickOutside)
  },
  unbind(el) {
    document.removeEventListener('click', el._clickOutside)
    delete el._clickOutside
  }
})

钩子执行顺序

以 v-if 控制的元素为例:

v-if="true" 时:
1. bind
2. inserted

v-if="false" 时:
1. unbind

再次 v-if="true" 时:
1. bind
2. inserted

完整生命周期示例

<div id="app">
  <button @click="show = !show">切换</button>
  <button @click="color = color === 'red' ? 'blue' : 'red'">改变颜色</button>
  
  <p v-if="show" v-demo="color">测试文本</p>
</div>

<script>
Vue.directive('demo', {
  bind(el, binding) {
    console.log('bind')
    el.style.color = binding.value
  },
  inserted(el, binding) {
    console.log('inserted')
  },
  update(el, binding) {
    console.log('update', binding.value, binding.oldValue)
    el.style.color = binding.value
  },
  componentUpdated(el, binding) {
    console.log('componentUpdated')
  },
  unbind(el) {
    console.log('unbind')
  }
})

new Vue({
  el: '#app',
  data: {
    show: true,
    color: 'red'
  }
})
</script>

钩子函数简写

如果 bind 和 update 需要相同逻辑:

Vue.directive('color', function(el, binding) {
  el.style.color = binding.value
})

等价于:

Vue.directive('color', {
  bind(el, binding) {
    el.style.color = binding.value
  },
  update(el, binding) {
    el.style.color = binding.value
  }
})

实际应用示例

防抖指令

Vue.directive('debounce', {
  bind(el, binding) {
    let timer = null
    el._debounce = function(event) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        binding.value(event)
      }, binding.arg || 300)
    }
    el.addEventListener('input', el._debounce)
  },
  unbind(el) {
    el.removeEventListener('input', el._debounce)
    delete el._debounce
  }
})

使用:

<input v-debounce:500="search" placeholder="搜索...">

懒加载指令

Vue.directive('lazy', {
  bind(el, binding) {
    el._observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value
          el._observer.unobserve(el)
        }
      })
    })
    el._observer.observe(el)
  },
  unbind(el) {
    if (el._observer) {
      el._observer.disconnect()
      delete el._observer
    }
  }
})

钩子选择指南

需求推荐钩子
初始化样式/属性bind
操作 DOM(focus、尺寸)inserted
响应数据变化update
等待子组件更新componentUpdated
清理资源unbind

理解钩子函数后,接下来学习钩子参数,获取指令的详细信息。