找回密码
 立即注册
首页 业界区 业界 [vue3] Vue3 自定义指令及原理探索

[vue3] Vue3 自定义指令及原理探索

梁丘艷蕙 2025-6-6 15:53:38


Vue3除了内置的v-on、v-bind等指令,还可以自定义指令。
注册自定义指令

全局注册
  1. const app = createApp({})
  2. // 使 v-focus 在所有组件中都可用
  3. app.directive('focus', {
  4.   /* ... */
  5. })
复制代码
局部选项式注册


在没有使用  [/code]实现自定义指令

指令的工作原理在于:在特定的时期为绑定的节点做特定的操作。
通过生命周期hooks实现自定义指令的逻辑。
  1. const myDirective = {
  2.   // 在绑定元素的 attribute 前
  3.   // 或事件监听器应用前调用
  4.   created(el, binding, vnode) {
  5.     // 下面会介绍各个参数的细节
  6.   },
  7.   // 在元素被插入到 DOM 前调用
  8.   beforeMount(el, binding, vnode) {},
  9.   // 在绑定元素的父组件
  10.   // 及他自己的所有子节点都挂载完成后调用
  11.   mounted(el, binding, vnode) {},
  12.   // 绑定元素的父组件更新前调用
  13.   beforeUpdate(el, binding, vnode, prevVnode) {},
  14.   // 在绑定元素的父组件
  15.   // 及他自己的所有子节点都更新后调用
  16.   updated(el, binding, vnode, prevVnode) {},
  17.   // 绑定元素的父组件卸载前调用
  18.   beforeUnmount(el, binding, vnode) {},
  19.   // 绑定元素的父组件卸载后调用
  20.   unmounted(el, binding, vnode) {}
  21. }
复制代码
其中最常用的是mounted和updated
简化形式
  1. app.directive('color', (el, binding) => {
  2.   // 这会在 `mounted` 和 `updated` 时都调用
  3.   el.style.color = binding.value
  4. })
复制代码
参数


  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。

  • vnode:代表绑定元素的底层 VNode。
  • prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
除了 el 外,其他参数都是只读的。
指令的工作原理

全局注册的指令

先看一下全局注册的指令。
全局注册是通过app的directive方法注册的,而app是通过createApp函数创建的。
源码位置:core/packages/runtime-core/src/apiCreateApp.ts at main · vuejs/core (github.com)
在createApp的实现中,可以看到创建了一个app对象,带有一个directive方法的实现,就是全局注册指令的API。
  1. const app: App = (context.app = {
  2.     ...
  3.     directive(name: string, directive?: Directive) {
  4.         if (__DEV__) {
  5.             validateDirectiveName(name)
  6.         }
  7.         if (!directive) {
  8.             return context.directives[name] as any
  9.         }
  10.         if (__DEV__ && context.directives[name]) {
  11.             warn(`Directive "${name}" has already been registered in target app.`)
  12.         }
  13.         context.directives[name] = directive
  14.         return app
  15.     },
  16.     ...
  17. })
复制代码
如代码中所示:

  • 如果调用app.directive(name),那么就会返回指定的指令对象;
  • 如果调用app.directive(name, directive),那么就会注册指定的指令对象,记录在context.directives对象上。
局部注册的指令

局部注册的指令会被记录在组件实例上。
源码位置:core/packages/runtime-core/src/component.ts at main · vuejs/core (github.com)
这里省略了大部分代码,只是想展示组件的instance上是有directives属性的,就是它记录着局部注册的指令。
  1. export function createComponentInstance(
  2.   vnode: VNode,
  3.   parent: ComponentInternalInstance | null,
  4.   suspense: SuspenseBoundary | null,
  5. ) {
  6.   ...
  7.   const instance: ComponentInternalInstance = {
  8.     ...
  9.     // local resolved assets
  10.     components: null,
  11.     directives: null,
  12.   }
  13.   ...
  14. }
复制代码
instance.directives被初始化为null,接下来我们看一下开发时注册的局部指令是如何被记录到这里的。
编译阶段

这一部分我还不太理解,但是大致找到了源码的位置:
core/packages/compiler-core/src/transforms/transformElement.ts at main · vuejs/core (github.com)
  1. // generate a JavaScript AST for this element's codegen
  2. export const transformElement: NodeTransform = (node, context) => {
  3.   // perform the work on exit, after all child expressions have been
  4.   // processed and merged.
  5.   return function postTransformElement() {
  6.     node = context.currentNode!
  7.         ......
  8.     // props
  9.     if (props.length > 0) {
  10.       const propsBuildResult = buildProps(
  11.         node,
  12.         context,
  13.         undefined,
  14.         isComponent,
  15.         isDynamicComponent,
  16.       )
  17.       ......
  18.       const directives = propsBuildResult.directives
  19.       vnodeDirectives =
  20.         directives && directives.length
  21.           ? (createArrayExpression(
  22.               directives.map(dir => buildDirectiveArgs(dir, context)),
  23.             ) as DirectiveArguments)
  24.           : undefined
  25.           ......
  26.     }
  27.     ......
  28.   }
  29. }
复制代码
大致就是通过buildProps获得了directives数组,然后记录到了vnodeDirectives。
buildProps中关于directives的源码大概在:core/packages/compiler-core/src/transforms/transformElement.ts at main · vuejs/core (github.com)
代码比较长,主要是先尝试匹配v-on、v-bind等内置指令并做相关处理,最后使用directiveTransform做转换:
  1. // buildProps函数的一部分代码
  2. //=====================================================================
  3. const directiveTransform = context.directiveTransforms[name]
  4. if (directiveTransform) {
  5.     // has built-in directive transform.
  6.     const { props, needRuntime } = directiveTransform(prop, node, context)
  7.     !ssr && props.forEach(analyzePatchFlag)
  8.     if (isVOn && arg && !isStaticExp(arg)) {
  9.         pushMergeArg(createObjectExpression(props, elementLoc))
  10.     } else {
  11.         properties.push(...props)
  12.     }
  13.     if (needRuntime) {
  14.         runtimeDirectives.push(prop)
  15.         if (isSymbol(needRuntime)) {
  16.             directiveImportMap.set(prop, needRuntime)
  17.         }
  18.     }
  19. } else if (!isBuiltInDirective(name)) {
  20.     // no built-in transform, this is a user custom directive.
  21.     runtimeDirectives.push(prop)
  22.     // custom dirs may use beforeUpdate so they need to force blocks
  23.     // to ensure before-update gets called before children update
  24.     if (hasChildren) {
  25.         shouldUseBlock = true
  26.     }
  27. }
复制代码
将自定义指令添加到runtimeDirectives里,最后作为buildProps的返回值之一。
  1. // buildProps函数的返回值
  2. //=====================================
  3. return {
  4.     props: propsExpression,
  5.     directives: runtimeDirectives,
  6.     patchFlag,
  7.     dynamicPropNames,
  8.     shouldUseBlock,
  9. }
复制代码
运行时阶段

这里介绍一下Vue3提供的一个关于template与渲染函数的网站:https://template-explorer.vuejs.org/
这里我写了一些简单的指令(事实上很不合理...就是随便写写):
template
  1.   <p
  2.     v-color="red"
  3.     v-capacity="0.8"
  4.     v-obj="{a:1, b:2}"
  5.     >
  6.       red font
  7.     </p>
复制代码
生成的渲染函数
  1. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  2.   const _directive_color = _resolveDirective("color")
  3.   const _directive_capacity = _resolveDirective("capacity")
  4.   const _directive_obj = _resolveDirective("obj")
  5.   const _directive_loading = _resolveDirective("loading")
  6.   return _withDirectives((_openBlock(), _createElementBlock("div", null, [
  7.     _withDirectives((_openBlock(), _createElementBlock("p", null, [
  8.       _createTextVNode(" red font ")
  9.     ])), [
  10.       [_directive_color, _ctx.red],
  11.       [_directive_capacity, 0.8],
  12.       [_directive_obj, {a:1, b:2}]
  13.     ])
  14.   ])), [
  15.     [_directive_loading, !_ctx.ready]
  16.   ])
  17. }
复制代码
这个网站还会在控制台输出AST,抽象语法树展开太占空间了,这里就不展示了。


  • _resolveDirective 函数根据指令名称在上下文中查找相应的指令定义,并返回一个指令对象。
  • _withDirectives(vnode, directives):将指令应用到虚拟节点 vnode 上。

    • directives:数组中的每个元素包含两个部分:指令对象和指令的绑定值。

resolveDirective

源码位置:core/packages/runtime-core/src/helpers/resolveAssets.ts at main · vuejs/core (github.com)
  1. export function resolveDirective(name: string): Directive | undefined {
  2.   return resolveAsset(DIRECTIVES, name)
  3. }
复制代码
调用了resolveAsset,在resolveAsset内部找到相关逻辑:(先找局部指令,再找全局指令)
  1. const res =
  2.       // local registration
  3.       // check instance[type] first which is resolved for options API
  4.       resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
  5.       // global registration
  6.       resolve(instance.appContext[type], name)
复制代码
resolve函数会尝试匹配原始指令名、驼峰指令名、首字母大写的驼峰:
  1. function resolve(registry: Record<string, any> | undefined, name: string) {
  2.   return (
  3.     registry &&
  4.     (registry[name] ||
  5.       registry[camelize(name)] ||
  6.       registry[capitalize(camelize(name))])
  7.   )
  8. }
复制代码
withDirective

源码位置:core/packages/runtime-core/src/directives.ts at main · vuejs/core (github.com)
  1. export function withDirectives<T extends VNode>(
  2.   vnode: T,
  3.   directives: DirectiveArguments,
  4. ): T {
  5.   // 如果当前没有渲染实例,说明该函数未在渲染函数内使用,给出警告
  6.   if (currentRenderingInstance === null) {
  7.     __DEV__ && warn(`withDirectives can only be used inside render functions.`)
  8.     return vnode
  9.   }
  10.   
  11.   // 获取当前渲染实例的公共实例
  12.   const instance = getComponentPublicInstance(currentRenderingInstance)
  13.   
  14.   // 获取或初始化 vnode 的指令绑定数组
  15.   const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
  16.   
  17.   // 遍历传入的指令数组
  18.   for (let i = 0; i < directives.length; i++) {
  19.     let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
  20.    
  21.     // 如果指令存在
  22.     if (dir) {
  23.       // 如果指令是一个函数,将其转换为对象形式的指令
  24.       if (isFunction(dir)) {
  25.         dir = {
  26.           mounted: dir,
  27.           updated: dir,
  28.         } as ObjectDirective
  29.       }
  30.       
  31.       // 如果指令具有 deep 属性,遍历其值
  32.       if (dir.deep) {
  33.         traverse(value)
  34.       }
  35.       
  36.       // 将指令绑定添加到绑定数组中
  37.       bindings.push({
  38.         dir,           // 指令对象
  39.         instance,      // 当前组件实例
  40.         value,         // 指令的绑定值
  41.         oldValue: void 0, // 旧值,初始为 undefined
  42.         arg,           // 指令参数
  43.         modifiers,     // 指令修饰符
  44.       })
  45.     }
  46.   }
  47.   
  48.   // 返回带有指令绑定的 vnode
  49.   return vnode
  50. }
复制代码
注意
  1. // 如果指令是一个函数,将其转换为对象形式的指令
  2. if (isFunction(dir)) {
  3.     dir = {
  4.         mounted: dir,
  5.         updated: dir,
  6.     } as ObjectDirective
  7. }
复制代码
这里就是上文提到的简便写法,传入一个函数,默认在mounted和updated这两个生命周期触发。
到这里,VNode就完成了指令的hooks的绑定。
在不同的生命周期,VNode会检查是否有指令回调,有的话就会调用。
生命周期的相关代码在renderer.ts文件里:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)
1.png
2.png
invokeDirectiveHook的实现在core/packages/runtime-core/src/directives.ts at main · vuejs/core (github.com),此处省略。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册