3. 钩子函数详解:bind、inserted、update、componentUpdated、unbind 的作用与触发时机

自定义指令的核心,说白了就是那五个钩子函数。很多初学者觉得它们长得差不多,用起来容易混淆。我刚开始学的时候也犯过这个错——在 bind 里操作 DOM,结果发现元素还没渲染出来,白忙活一场。

今天咱们就把这五个钩子一个一个掰开揉碎了讲。你想想看,搞懂了它们的触发时机,写指令就跟玩似的。

3.1 bind() —— 指令第一次绑定到元素时

bind 只调用一次。就在指令第一次绑定到元素上时触发。这时候元素还没插入到父节点里,更没出现在页面上。

我个人习惯把一些初始化的工作放在这里。比如绑定事件监听器、设置初始值、或者做一些简单的计算。

触发时机:指令与元素绑定成功,但元素尚未插入 DOM。

典型用途:初始化配置、绑定事件、设置样式。

// 一个简单的 v-highlight 指令
Vue.directive('highlight', {
  bind(el, binding) {
    // 这时候 el 还没在页面上,但我们可以准备样式
    el.style.backgroundColor = binding.value || 'yellow';
    console.log('bind 触发:指令绑定了,但元素还没渲染');
  }
});

嗯,这里要注意:bind 里拿不到父节点。如果你需要操作父元素,别在这里做。

3.2 inserted() —— 元素被插入父节点时

inserted 也是只调用一次。但它是在元素被插入到父节点后触发的。注意,这里说的是父节点,不保证元素已经在页面上可见。

我在项目中遇到过一个问题:用 bind 去获取元素的 offsetWidth,结果永远是 0。后来才意识到,元素还没插入 DOM,哪来的宽高?换成 inserted 就好了。

小技巧:如果你需要获取元素的尺寸、位置等布局信息,用 inserted 而不是 bind

Vue.directive('measure', {
  inserted(el) {
    // 这时候元素已经在父节点里了
    console.log('元素宽度:', el.offsetWidth);
    console.log('元素高度:', el.offsetHeight);
  }
});

说白了,bindinserted 的区别就一句话:一个在 DOM 外,一个在 DOM 内。

3.3 update() —— 组件更新时,但子组件可能还没更新

update 在组件更新时触发,但这时候子组件可能还没更新完。它会在每次虚拟 DOM 重新渲染时被调用,不管绑定的值有没有变化。

为什么会这样?因为 Vue 的更新机制是父组件先更新,然后才轮到子组件。所以 update 里拿到的子组件状态可能还是旧的。

注意:不要在这里做依赖子组件 DOM 的操作。如果你需要确保子组件也更新完了,用 componentUpdated

Vue.directive('log-update', {
  update(el, binding) {
    // 每次父组件更新都会触发,不管值变没变
    console.log('update 触发,当前值:', binding.value);
    // 注意:子组件可能还没更新
  }
});

我曾经踩过一个坑:在 update 里更新子组件的样式,结果子组件还没渲染完,样式就丢了。后来改成 componentUpdated 才解决。

3.4 componentUpdated() —— 组件和子组件都更新完

componentUpdatedupdate 的升级版。它确保当前组件和所有子组件都更新完毕后才触发。说白了,就是等大家都忙完了,再通知你。

我个人习惯把需要依赖完整 DOM 状态的操作放在这里。比如更新滚动位置、重新计算布局、或者做一些动画的收尾工作。

触发时机:组件及其所有子组件都完成更新后。

典型用途:依赖完整 DOM 状态的操作,如滚动定位、布局计算。

Vue.directive('scroll-to', {
  componentUpdated(el, binding) {
    // 确保列表更新完后,滚动到指定位置
    const index = binding.value;
    const target = el.children[index];
    if (target) {
      target.scrollIntoView({ behavior: 'smooth' });
    }
  }
});

嗯,这里要注意:componentUpdated 可能会被频繁调用。如果你的操作比较重,记得加个防抖。

3.5 unbind() —— 指令与元素解绑时

unbind 只调用一次。当指令从元素上解绑时触发。通常发生在组件销毁或者元素被移除的时候。

这个钩子最重要的作用就是——清理现场。我在项目中见过不少内存泄漏,都是因为忘了在 unbind 里移除事件监听器。

避坑指南:我曾经因为没在 unbind 里移除全局事件监听,导致页面切换后还在触发旧组件的事件。排查了半天才发现是这个问题。

Vue.directive('event-logger', {
  bind(el) {
    // 绑定事件
    el._clickHandler = () => console.log('点击了');
    el.addEventListener('click', el._clickHandler);
  },
  unbind(el) {
    // 一定要移除,不然会内存泄漏
    el.removeEventListener('click', el._clickHandler);
    delete el._clickHandler;
    console.log('unbind 触发:事件已清理');
  }
});

3.6 五个钩子的触发时机对比

为了方便记忆,我整理了一张表。你把它存下来,写指令的时候对照着看就行。

钩子函数 调用次数 触发时机 DOM 状态
bind 1 次 指令第一次绑定到元素 未插入父节点
inserted 1 次 元素被插入父节点 已插入父节点
update 多次 组件更新时(子组件可能未更新) 可能不完整
componentUpdated 多次 组件和子组件都更新完 完整
unbind 1 次 指令与元素解绑 即将被移除

我的建议:写指令时先想清楚你要在哪个阶段干活。初始化用 bind,操作 DOM 用 inserted,响应更新用 componentUpdated,清理用 unbind。至于 update,除非你明确知道自己在做什么,否则尽量用 componentUpdated 替代。

好了,这五个钩子就讲完了。你想想看,是不是比想象中简单?说白了就是一套生命周期:绑定 → 插入 → 更新 → 更新完成 → 解绑。记住这个顺序,写指令就不会乱了。