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

cnblogs 2024-08-01 08:11:04 阅读 97

这篇文章记录了在Vue3中如何在全局或者组件内部注册自定义事件,并通过阅读源码探索自定义事件的实现原理。

Vue3除了内置的<code>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) {}

}

其中最常用的是mountedupdated

简化形式

app.directive('color', (el, binding) => {

// 这会在 `mounted` 和 `updated` 时都调用

el.style.color = binding.value

})

参数

  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive="1 + 1"code> 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

除了 el 外,其他参数都是只读的。

指令的工作原理

全局注册的指令

先看一下全局注册的指令。

全局注册是通过appdirective方法注册的,而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-onv-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">code>

<p

v-color="red" code>

v-capacity="0.8"code>

v-obj="{a:1, b:2}"code>

>

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

}

这里就是上文提到的简便写法,传入一个函数,默认在mountedupdated这两个生命周期触发。

到这里,VNode就完成了指令的hooks的绑定。

在不同的生命周期,VNode会检查是否有指令回调,有的话就会调用。

生命周期的相关代码在renderer.ts文件里:core/packages/runtime-core/src/renderer.ts at main · vuejs/core (github.com)

image-20240801020620698

image-20240801020647645

<code>invokeDirectiveHook的实现在core/packages/runtime-core/src/directives.ts at main · vuejs/core (github.com),此处省略。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。