- A+
Vue3除了内置的v-on
、v-bind
等指令,还可以自定义指令。
注册自定义指令
全局注册
const app = createApp({}) // 使 v-focus 在所有组件中都可用 app.directive('focus', { /* ... */ })
局部选项式注册
在没有使用<script setup>
的情况下,使用选项式语法,在direactives
中注册事件。
export default { setup() { /*...*/ }, directives: { // 在模板中启用 v-focus focus: { /* ... */ } } }
隐式注册
在<script setup>
内,任何以v
开头并遵循驼峰式命名的变量都可以用作一个自定义指令。
<script setup> // 在模板中启用 v-focus const vFocus = { mounted: (el) => el.focus() } </script> <template> <input v-focus /> </template>
实现自定义指令
指令的工作原理在于:在特定的时期为绑定的节点做特定的操作。
通过生命周期hooks
实现自定义指令的逻辑。
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
。
简化形式:
app.directive('color', (el, binding) => { // 这会在 `mounted` 和 `updated` 时都调用 el.style.color = binding.value })
参数
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。
const app: App = (context.app = { ... 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 }, ... })
如代码中所示:
- 如果调用
app.directive(name)
,那么就会返回指定的指令对象; - 如果调用
app.directive(name, directive)
,那么就会注册指定的指令对象,记录在context.directives
对象上。
局部注册的指令
局部注册的指令会被记录在组件实例上。
源码位置:core/packages/runtime-core/src/component.ts at main · vuejs/core (github.com)
这里省略了大部分代码,只是想展示组件的instance
上是有directives
属性的,就是它记录着局部注册的指令。
export function createComponentInstance( vnode: VNode, parent: ComponentInternalInstance | null, suspense: SuspenseBoundary | null, ) { ... const instance: ComponentInternalInstance = { ... // local resolved assets components: null, directives: null, } ... }
instance.directives
被初始化为null
,接下来我们看一下开发时注册的局部指令是如何被记录到这里的。
编译阶段
这一部分我还不太理解,但是大致找到了源码的位置:
core/packages/compiler-core/src/transforms/transformElement.ts at main · vuejs/core (github.com)
// generate a JavaScript AST for this element's codegen export const transformElement: NodeTransform = (node, context) => { // perform the work on exit, after all child expressions have been // processed and merged. return function postTransformElement() { node = context.currentNode! ...... // props if (props.length > 0) { const propsBuildResult = buildProps( node, context, undefined, isComponent, isDynamicComponent, ) ...... const directives = propsBuildResult.directives vnodeDirectives = directives && directives.length ? (createArrayExpression( directives.map(dir => buildDirectiveArgs(dir, context)), ) as DirectiveArguments) : undefined ...... } ...... } }
大致就是通过buildProps
获得了directives
数组,然后记录到了vnodeDirectives
。
buildProps
中关于directives
的源码大概在:core/packages/compiler-core/src/transforms/transformElement.ts at main · vuejs/core (github.com)
代码比较长,主要是先尝试匹配v-on
、v-bind
等内置指令并做相关处理,最后使用directiveTransform
做转换:
// buildProps函数的一部分代码 //===================================================================== const directiveTransform = context.directiveTransforms[name] if (directiveTransform) { // has built-in directive transform. const { props, needRuntime } = directiveTransform(prop, node, context) !ssr && props.forEach(analyzePatchFlag) if (isVOn && arg && !isStaticExp(arg)) { pushMergeArg(createObjectExpression(props, elementLoc)) } else { properties.push(...props) } if (needRuntime) { runtimeDirectives.push(prop) if (isSymbol(needRuntime)) { directiveImportMap.set(prop, needRuntime) } } } else if (!isBuiltInDirective(name)) { // no built-in transform, this is a user custom directive. runtimeDirectives.push(prop) // custom dirs may use beforeUpdate so they need to force blocks // to ensure before-update gets called before children update if (hasChildren) { shouldUseBlock = true } }
将自定义指令添加到runtimeDirectives
里,最后作为buildProps
的返回值之一。
// buildProps函数的返回值 //===================================== return { props: propsExpression, directives: runtimeDirectives, patchFlag, dynamicPropNames, shouldUseBlock, }
运行时阶段
这里介绍一下Vue3
提供的一个关于template
与渲染函数的网站:https://template-explorer.vuejs.org/
这里我写了一些简单的指令(事实上很不合理...就是随便写写):
template
<div v-loading="!ready"> <p v-color="red" v-capacity="0.8" v-obj="{a:1, b:2}" > red font </p> </div>
生成的渲染函数:
export function render(_ctx, _cache, $props, $setup, $data, $options) { const _directive_color = _resolveDirective("color") const _directive_capacity = _resolveDirective("capacity") const _directive_obj = _resolveDirective("obj") const _directive_loading = _resolveDirective("loading") return _withDirectives((_openBlock(), _createElementBlock("div", null, [ _withDirectives((_openBlock(), _createElementBlock("p", null, [ _createTextVNode(" red font ") ])), [ [_directive_color, _ctx.red], [_directive_capacity, 0.8], [_directive_obj, {a:1, b:2}] ]) ])), [ [_directive_loading, !_ctx.ready] ]) }
这个网站还会在控制台输出AST,抽象语法树展开太占空间了,这里就不展示了。
_resolveDirective
函数根据指令名称在上下文中查找相应的指令定义,并返回一个指令对象。_withDirectives(vnode, directives)
:将指令应用到虚拟节点vnode
上。directives
:数组中的每个元素包含两个部分:指令对象和指令的绑定值。
resolveDirective
源码位置:core/packages/runtime-core/src/helpers/resolveAssets.ts at main · vuejs/core (github.com)
export function resolveDirective(name: string): Directive | undefined { return resolveAsset(DIRECTIVES, name) }
调用了resolveAsset
,在resolveAsset
内部找到相关逻辑:(先找局部指令,再找全局指令)
const res = // local registration // check instance[type] first which is resolved for options API resolve(instance[type] || (Component as ComponentOptions)[type], name) || // global registration resolve(instance.appContext[type], name)
resolve
函数会尝试匹配原始指令名、驼峰指令名、首字母大写的驼峰:
function resolve(registry: Record<string, any> | undefined, name: string) { return ( registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]) ) }
withDirective
源码位置:core/packages/runtime-core/src/directives.ts at main · vuejs/core (github.com)
export function withDirectives<T extends VNode>( vnode: T, directives: DirectiveArguments, ): T { // 如果当前没有渲染实例,说明该函数未在渲染函数内使用,给出警告 if (currentRenderingInstance === null) { __DEV__ && warn(`withDirectives can only be used inside render functions.`) return vnode } // 获取当前渲染实例的公共实例 const instance = getComponentPublicInstance(currentRenderingInstance) // 获取或初始化 vnode 的指令绑定数组 const bindings: DirectiveBinding[] = 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, } as ObjectDirective } // 如果指令具有 deep 属性,遍历其值 if (dir.deep) { traverse(value) } // 将指令绑定添加到绑定数组中 bindings.push({ dir, // 指令对象 instance, // 当前组件实例 value, // 指令的绑定值 oldValue: void 0, // 旧值,初始为 undefined arg, // 指令参数 modifiers, // 指令修饰符 }) } } // 返回带有指令绑定的 vnode return vnode }
注意:
// 如果指令是一个函数,将其转换为对象形式的指令 if (isFunction(dir)) { dir = { mounted: dir, updated: dir, } as ObjectDirective }
这里就是上文提到的简便写法,传入一个函数,默认在mounted
和updated
这两个生命周期触发。
到这里,VNode就完成了指令的hooks的绑定。
在不同的生命周期,VNode会检查是否有指令回调,有的话就会调用。
生命周期的相关代码在renderer.ts
文件里:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)
invokeDirectiveHook
的实现在core/packages/runtime-core/src/directives.ts at main · vuejs/core (github.com),此处省略。