[vue3] Vue3 自定義指令及原理探索
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),此處省略。

浙公網安備 33010602011771號