[vue3] Vue3 自定义指令及原理探索
Vue3除了内置的v-on、v-bind等指令,还可以自定义指令。
注册自定义指令
全局注册
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})局部选项式注册
在没有使用实现自定义指令
指令的工作原理在于:在特定的时期为绑定的节点做特定的操作。
通过生命周期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 as any
}
if (__DEV__ && context.directives) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives = 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
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
<p
v-color="red"
v-capacity="0.8"
v-obj="{a:1, b:2}"
>
red font
</p>生成的渲染函数:
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 ")
])), [
,
,
])
])), [
])
}这个网站还会在控制台输出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 first which is resolved for options API
resolve(instance || (Component as ComponentOptions), name) ||
// global registration
resolve(instance.appContext, name)resolve函数会尝试匹配原始指令名、驼峰指令名、首字母大写的驼峰:
function resolve(registry: Record<string, any> | undefined, name: string) {
return (
registry &&
(registry ||
registry ||
registry)
)
}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 = directives
// 如果指令存在
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),此处省略。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]