Jinuss's blog Jinuss's blog
首页
  • 源码合集

    • Leaflet源码分析
    • Openlayers源码合集
    • vue3源码
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

Web、WebGIS技术博客
首页
  • 源码合集

    • Leaflet源码分析
    • Openlayers源码合集
    • vue3源码
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • reactivity响应式

  • runtime-core运行时核心模块

    • watch和watcherEffect源码解析
    • scheduler调度器
    • directive自定义指令实现原理
    • 5.18源码学习》
    • runtime-core运行时核心模块
    东流
    2025-09-12
    目录

    directive自定义指令实现原理

    # 概述

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

    # 指令

    # 指令钩子

    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) {},
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 自定义防抖指令

    # 定义指令

    一般地,我们只需要在 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间隔
        }
      });
    },
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

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

    # 指令的注册

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

    app.directive("debounce", debounce);
    
    1

    # 使用指令

    <el-button v-debounce="() => search(formRef)" type="primary">
      搜索
    </el-button>
    
    1
    2
    3

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

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

    指令对应修改如下:

    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间隔
        }
      });
    },
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    # 注意事项

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

    # directive 源码分析

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

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

    # 指令注册

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

      directive(name: string, directive?: Directive) {
            if (!directive) {
              return context.directives[name]
            }
            context.directives[name] = directive
            return app
      },
    
    1
    2
    3
    4
    5
    6
    7

    # 指令绑定

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

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

    runtimeDom.registerRuntimeCompiler(compileToFunction);
    
    1

    编译器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,
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    在baseCompile函数中会进一步合并directiveTransforms,主要包括on、bind、model, 如上注释会重写覆盖DOMDirectiveTransforms的on和model对应的的transform函数,此外如果baseCompile的第一个参数source是字符串,就还会调用baseParse解析函数,它会将source解析成ast语法树,其中也包括自定义的指令相关字符串。接着,会调用transform函数,在这个函数就会调用一个递归函数traverseNode会去遍历其子节点以及遍历执行对应的nodeTransforms,而nodeTransforms就是和directiveTransforms同时传进来的,而指令的逻辑就在其中的transformElement中进行的。

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

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

    function withDirectives(vnode, directives) {
      // 先判断当前渲染实例是否存在,若不存在,则直接返回vnode
      if (currentRenderingInstance === null) {
        return vnode;
      }
      // 获取当前渲染实例的暴露的代理
      const instance = getExposeProxy(currentRenderingInstance)
      // 获取vnode上的指令数组dirs
      const bindings = vnode.dirs || (vnode.dirs = []);
      // 遍历指令集directives
      for (let i = 0; i < directives.length; i++) {
        let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i];
        if (dir) {
          // 判断dir是否是函数
          if (shared.isFunction(dir)) {
            dir = {
              mounted: dir, //挂载后
              updated: dir, // 更新后
            };
          }
          // 判断dir的deep是否为true,
          if (dir.deep) {
            // 调用traverse进行深度遍历
            reactivity.traverse(value);
          }
          // 将指令集中的内容放到vnode的dirs中
          bindings.push({
            dir,
            instance,
            value,
            oldValue: void 0,
            arg,
            modifiers,
          });
        }
      }
      return vnode;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    # 指令执行

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

    dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
    
    1

    而invokeDirectiveHook函数地定义如下,会根据参数name从vnode.dirs中取指令对应地生命周期函数,并传参,调用callWithAsyncErrorHanding执行,实际就是执行hook(vnode.el,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 (hook) {
          pauseTracking();
          // 停止追踪
          callWithAsyncErrorHandling(hook, instance, 8 /* DIRECTIVE_HOOK */, [
            vnode.el,
            binding,
            vnode,
            prevVNode,
          ]);
          // 恢复之前的追踪状态
          resetTracking();
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    invokeDirectiveHook的name对应组件的生命周期钩子,也是对应指令的周期钩子函数,其值可能为created/beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted。

    但是需要注意的一点是,在withDirectives中解析自定义指令时,若自定义指令是一个函数,则只有mounted和updated钩子是注册了,即组件只有触发mounted或updated时,才会触发自定义指令函数,其余钩子函数被触发时,自定义指令函数不会被执行;而当自定义指令是一个对象时,组件触发,会间接触发自定义指令对象中对应的同名函数。

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

    编辑 (opens new window)
    上次更新: 2025/09/12, 09:24:28
    scheduler调度器

    ← scheduler调度器

    最近更新
    01
    scheduler调度器
    09-10
    02
    watch和watcherEffect源码解析
    09-09
    03
    响应式中的watch实现
    09-08
    更多文章>
    Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式