一文带你读懂vue3中directive指令的那些事

时间:2025-04-11 14:57:15

概述

vue3 中内置了很多丰富实用的指令,如v-showv-if/v-elsev-model等,但是实际开发中可能我们还需要某些统一的处理,比如交互按钮的防抖,输入框的自动focus等,这时我们就可以通过vue3directive注册自定义指令。

指令

指令钩子

vue3的自定义指令通常情况下是由一个包含类似组件生命周期钩子函数的对象,在DOM节点不同的时期,执行不同的钩子函数,而我们就可以在对应的钩子函数中接收到DOM节点、实例instance等待处理一些业务逻辑。

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {},
};

自定义防抖指令

定义指令

一般地,我们只需要在 mounted中或updated中去处理,一个防抖指令如下:

export default {
mounted(el, binding) {
  if (typeof binding.value !== 'function') {
    throw new Error("debounce指令的参数必须是一个函数,延时为1500ms")
    return
  }
  // 初始化时监听
  el.addEventListener('click', () => {
    if (!el.disabled) {
      el.disabled = true;
      const timer = setTimeout(() => {
        el.disabled = false;
        binding.value()
        clearTimeout(timer)
      }, 1000) // 1s间隔
    }
  });
},

上面的防抖指令debounce实现的就是click事件延迟一秒触发,当然时间间隔也可以当作参数传递,使用的时候:

指令的注册

大多数情况还是会选择全局注册指令,挂载到 App 的全局上下文中去

app.directive("debounce", debounce);
使用指令
<el-button v-debounce="() => search(formRef)" type="primary">
  搜索
</el-button>

时间间隔使用参数传递,我们也可以这样写:

<el-button
  v-debounce="{ event: () => search(formRef), time: 1500 }"
  type="primary"
>
  搜索
</el-button>

指令对应修改如下:

export default {
mounted(el, binding) {
  const {event,time}=binding.value;
  if (typeof event !== 'function') {
    throw new Error("debounce指令的参数必须是一个函数,延时为1500ms")
    return
  }
  // 初始化时监听
  el.addEventListener('click', () => {
    if (!el.disabled) {
      el.disabled = true;
      const timer = setTimeout(() => {
        el.disabled = false;
        event()
        clearTimeout(timer)
      }, time) // 1s间隔
    }
  });
},
注意事项

官网上说:"只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind 这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。” 毫无疑问,自定义指令是会带来性能的消耗,使用时应该有所权衡

directive 源码分析

vue3使用自定义指令就三步:1.定义指令,构造特殊的对象 2.指令注册 3.指令绑定到DOM节点。

directive的源码就从全局注册开始

指令注册

当调用全局注册自定义指令时,会执行下面这个directive函数,将指令挂载到全局上下文context上去,所有的自定义指令保存在中。

  directive(name: string, directive?: Directive) {
        if (__DEV__) {
          validateDirectiveName(name) //判断是否是内部指令
        }

        if (!directive) {
          return context.directives[name] as any
        }
        if (__DEV__ && context.directives[name]) {
          warn(`Directive "${name}" has already been registered in target app.`)
        }
        context.directives[name] = directive
        return app
  },
指令绑定

指令绑定在DOM或者Node节点上,肯定就会涉及到模板的编译。

vue3首先会通过@vue/runtime-domregisterRuntimeCompiler方法将编译器注册到运行时,这样vue就可以在运行时编译模板字符串。

runtimeDom.registerRuntimeCompiler(compileToFunction);

编译器compileToFunction的作用就是将模板字符串编译成渲染函数,而其内部会调用@vue/compiler-dom模块的compile函数将字符串模板template编译为代码。compile接收template作为其中一个参数,返回baseCompile函数的执行结果,而baseCompile函数的第二个参数是个对象,它会有一个属性directiveTransforms,初始时它的值是包含和DOM节点有关的transform方法,如下:

const DOMDirectiveTransforms = {
  cloak: noopDirectiveTransform,
  html: transformVHtml,
  text: transformVText,
  model: transformModel,
  // override compiler-core
  on: transformOn,
  // override compiler-core
  show: transformShow,
};

baseCompile函数中会进一步合并directiveTransforms,主要包括onbindmodel, 如上注释会重写覆盖DOMDirectiveTransformsonmodel对应的的transform函数,此外如果baseCompile的第一个参数source是字符串,就还会调用baseParse解析函数,它会将source解析成ast语法树,其中也包括自定义的指令相关字符串。

接着,会调用transform函数,在这个函数就会调用一个递归函数traverseNode会去遍历其子节点以及遍历执行对应的nodeTransforms,而nodeTransforms就是和directiveTransforms同时传进来的,而指令的逻辑就在其中的transformElement中进行的。

transformElement 接受两个参数 nodecontext。当 node 的属性 props 长度不为 0 时,就会调用 buildProps 函数,buildProps 函数顾名思义就是获取 node 上的属性进行相应操作,而针对指令,就会先判断是不是自定义指令,如果是自定义指令则将 prop 放进 runtimeDirectives ;如果不是自定义指令,则调用指令自身的 transform。

buildProps 函数调用完成后,返回一个名为 propsBuildResult 对象,而指令集都包含在 中,然后调用 createVNodeCall, 在 createVNodeCall 中就会调用 withDirectives 方法,这个方法会将指令结构出来的argexpmodifiers等 添加到 VNodedirs 上,下面是它的实现:

function withDirectives(vnode, directives) {
  if (currentRenderingInstance === null) {
    warn2(`withDirectives can only be used inside render functions.`);
    return vnode;
  }
  const instance =
    getExposeProxy(currentRenderingInstance) || currentRenderingInstance.proxy;
  const bindings = vnode.dirs || (vnode.dirs = []);
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i];
    if (dir) {
      if (isFunction(dir)) {
        dir = {
          mounted: dir,
          updated: dir,
        };
      }
      if (dir.deep) {
        traverse(value);
      }
      bindings.push({
        dir,
        instance,
        value,
        oldValue: void 0,
        arg,
        modifiers,
      });
    }
  }
  return vnode;
}
指令执行

指令的执行就简单多了,指令中对应createdmounted等待生命周期函数,那么只需要在VNode对应的生命周期时去判断该节点有没有指令即其dir是否有值,若有值则执行对应的invokeDirectiveHook,比如如下代码就是mountElement中的一个片段,mountElement会在element或是VNode挂载时执行

if (
  (vnodeHook = props && props.onVnodeMounted) ||
  needCallTransitionHooks ||
  dirs
) {
  queuePostRenderEffect(() => {
    vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
    needCallTransitionHooks && transition.enter(el);
    dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
  }, parentSuspense);
}

invokeDirectiveHook函数地定义如下,会根据参数name中取指令对应地生命周期函数,并传参,调用callWithAsyncErrorHanding执行,实际就是执行hook(,binding,vnode,preVNode)

function invokeDirectiveHook(vnode, prevVNode, instance, name) {
  const bindings = vnode.dirs;
  const oldBindings = prevVNode && prevVNode.dirs;
  for (let i = 0; i < bindings.length; i++) {
    const binding = bindings[i];
    if (oldBindings) {
      binding.oldValue = oldBindings[i].value;
    }
    let hook = binding.dir[name];
    if (false) {
      hook = mapCompatDirectiveHook(name, binding.dir, instance);
    }
    if (hook) {
      pauseTracking();
      callWithAsyncErrorHandling(hook, instance, 8 /* DIRECTIVE_HOOK */, [
        vnode.el,
        binding,
        vnode,
        prevVNode,
      ]);
      resetTracking();
    }
  }
}

至此vue3中关于directive指令就介绍到这里了,后续会根据理解的加深予以补充修正。