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

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

注册自定义指令

全局注册

const app = createApp({})// 使 v-focus 在所有组件中都可用
app.directive('focus', {/* ... */
})

局部选项式注册

在没有使用<script setup>的情况下,使用选项式语法,在direactives中注册事件。

export default {setup() {/*...*/},directives: {// 在模板中启用 v-focusfocus: {/* ... */}}
}

隐式注册

<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" 中,值是 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] = directivereturn 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 assetscomponents: 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!......// propsif (props.length > 0) {const propsBuildResult = buildProps(node,context,undefined,isComponent,isDynamicComponent,)......const directives = propsBuildResult.directivesvnodeDirectives =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 updateif (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 APIresolve(instance[type] || (Component as ComponentOptions)[type], name) ||// global registrationresolve(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, // 旧值,初始为 undefinedarg,           // 指令参数modifiers,     // 指令修饰符})}}// 返回带有指令绑定的 vnodereturn 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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/775886.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于在windows系统下使用Linux子系统

今天意外刷到一个短视频,介绍了如何在windows下方便的使用系统自带的Linux子系统,本人抱着好奇的心理,也因为最近碰到了只使用windows操作系统解决不了的问题,还有想到以后测试项目大概率也要在Linux系统下进行。除此之外,本人之前也尝试过使用类似于Vmware等大型虚拟机来…

特性

1.特性的定义:特性就是一个类,必须直接或者间接继承Attribute 特性本身也会被修饰可以用在哪个地方: 如果特性本身的修饰为All : [AttributeUsage(AttributeTargets.All,AllowMultiple =true)] ,则都可修饰 2.特性的访问:特性本身是没有用的,不影响程序的运行;我们只能…

【PHP系列】内存马(二)不死马优化

实验环境工具 phpstudy传统的PHP不死马就是一根筋的往一处写文件,这样就会造成不管文件有没有被修改或者文件是否存在,都会重新创建一个文件,这样会比较占用服务器资源,所以就可以通过这点来对不死马进行优化。 优化前: <?phpset_time_limit(0);ignore_user_abort(1);…

Zotero怎样才能形成[2-6]这样的引用

Step01 在Word中引用多篇文献 点击“Add/Edit Citation” 在跳出的Zotero搜索框中,选择“经典视图” 在跳出的“添加/编辑引注”界面中,点击“多重来源”(单一来源) 选中对应的文章后,添加即可、添加成功的效果如下:可以看到[2-6]Step02 在参考文献部分显示 这里针对的是…

zotero参考文献双语引用

实现这样的效果:真服了,今天急着排版,英文是自己对着参考文献一篇一篇敲上去的...

直流电机

电机分类 有刷直流电机工作原理下面这个是LearnEngineering制作的动画,讲解的是直流电机的工作原理,就是把电枢线圈中感应产生的交变电动势,靠换向器配合电刷的换向作用,使之从电刷端引出时变为直流电动势的原理。直流有刷电机是大家最早接触的一类电机,中学时物理课堂上…

Windows 10 如何禁止 自动更新

windows 10 如何禁止 自动更新关闭Windows 10自动更新的方法有多种,‌包括通过Windows设置、‌使用第三方工具、‌禁用Windows Update服务、‌使用本地组策略编辑器、‌任务计划程序以及编辑注册表等。‌通过Windows设置关闭自动更新:‌打开设置应用,‌点击更新与安全,‌选…

我用Awesome-Graphs看论文:解读GraphBolt

这次向大家分享一篇流图处理系统论文GraphBolt,看如何基于计算历史的方式实现增量图计算,并保证与全量图计算语义的一致性。GraphBolt论文:《GraphBolt: Dependency-Driven Synchronous Processing of Streaming Graphs》前面通过文章《论文图谱当如是:Awesome-Graphs用200…

vue部署到iis

下载安装两个IIS模块 URL Rewritehttps://www.iis.net/downloads/microsoft/url-rewriteApplication Request Routinghttps://www.iis.net/downloads/microsoft/application-request-routing安装完成配置 添加网站 物理路径是打包后的文件位置,即dist文件夹:配置网站下的URL …

Java内存区域与内存溢出异常 - 运行时数据区

一、运行时数据区1.1 程序计数器 - 线程私有 可以看做当前线程所执行的字节码行号指示器,在任意时刻一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。所以为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的线程计数器,各条线程之间…

在 Hub 上使用 Presidio 进行自动 PII 检测实验

我们在 Hugging Face Hub 上托管的机器学习 (ML) 数据集中发现了一个引人关注的现象: 包含个人未经记录的私密信息。这一现象为机器学习从业者带来了一些特殊挑战。 在本篇博客中,我们将深入探讨含有一种称为个人识别信息 (PII) 的私密信息的各类数据集,分析这些数据集存在的…

大厂面经: 字节跳动 iOS开发实习生-飞书

好家伙, 线上面试,总时长1h30mins左右 整体流程: 0.自我介绍(0-2mins) 1.做的比较难的事情(15min) 我讲我之前写的一个低开平台,写了一个撤销回退功能,提了个pr,用了节流,用了命令模式 1.1.节流怎么用的?用在哪?为什么用? 1.2.节流把前面的触发直接clean了还是怎么处理…