第206集:Vue 3指令系统源码深度解析
概述
在本集中,我们将深入剖析Vue 3指令系统的源码实现。指令是Vue模板中用于操作DOM的特殊标记,如v-if、v-for、v-bind等。理解指令系统的实现机制对于掌握Vue 3模板编译和运行时至关重要。
指令系统核心架构
Vue 3指令系统主要包含以下核心模块:
- 指令定义:定义指令的结构和行为
- 指令注册:注册内置指令和自定义指令
- 指令编译:将模板中的指令转换为渲染函数
- 指令运行时:在运行时执行指令逻辑
- 内置指令:Vue内置的常用指令
- 自定义指令:支持开发者自定义指令
源码目录结构
Vue 3指令系统的源码主要位于以下目录:
packages/
├── compiler-core/src/ # 核心编译逻辑,包含指令编译
│ ├── directives/ # 编译时指令处理
│ └── parse.ts # 模板解析,处理指令
└── runtime-core/src/ # 运行时核心逻辑
├── directives/ # 运行时指令实现
└── component.ts # 组件中指令的处理核心源码解析
1. 指令定义与注册
内置指令定义
内置指令定义在runtime-core/src/directives/目录下,例如v-model的定义:
// packages/runtime-core/src/directives/vModel.ts
/**
* v-model指令实现
*/
export const vModel: ObjectDirective = {
created(el, { value, modifiers: { lazy, number, trim } }, vnode) {
// 初始化v-model
// ...
},
mounted(el, { value, modifiers: { lazy, number, trim } }, vnode) {
// 挂载v-model
// ...
},
beforeUpdate(el, { value, oldValue, modifiers: { lazy, number, trim } }, vnode) {
// 更新前处理
// ...
},
updated(el, { value, modifiers: { lazy, number, trim } }, vnode) {
// 更新后处理
// ...
},
beforeUnmount(el, binding, vnode) {
// 卸载前清理
// ...
}
}指令注册
指令注册的核心逻辑在runtime-core/src/apiDirective.ts中:
// packages/runtime-core/src/apiDirective.ts
/**
* 注册指令
* @param app 应用实例
* @param name 指令名称
* @param directive 指令对象
*/
export function registerDirective(
app: App,
name: string,
directive: Directive
) {
// 标准化指令
const normalizedDirective = normalizeDirective(directive)
// 注册到应用的directives中
app.directives[name] = normalizedDirective
}
/**
* 标准化指令
* @param directive 指令对象
* @returns 标准化后的指令
*/
function normalizeDirective(directive: Directive): Directive {
// 如果是函数,转换为对象形式
if (isFunction(directive)) {
return {
mounted: directive,
updated: directive
}
}
return directive
}2. 指令编译
指令编译的核心逻辑在编译器中,负责将模板中的指令转换为渲染函数。让我们看一下compiler-core/src/directives/目录下的实现:
v-if指令编译
// packages/compiler-core/src/directives/vIf.ts
/**
* 编译v-if指令
* @param node 当前节点
* @param dir 指令对象
* @param context 编译上下文
* @returns 编译结果
*/
export const transformIf = createStructuralDirectiveTransform(
'if',
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// 处理if分支
if (isRoot) {
ifNode.codegenNode = branch
} else {
// 处理else-if和else分支
// ...
}
})
}
)
/**
* 处理if指令
* @param node 当前节点
* @param dir 指令对象
* @param context 编译上下文
* @param processCodegen 处理代码生成的回调
*/
function processIf(
node: ElementNode | ForNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen: (node: IfNode, branch: IfBranchNode, isRoot: boolean) => void
) {
// 创建if节点
const ifNode: IfNode = {
type: NodeTypes.IF,
branches: [],
loc: node.loc
}
// 创建if分支
const branch: IfBranchNode = {
type: NodeTypes.IF_BRANCH,
condition: dir.exp,
children: [node],
loc: node.loc
}
// 添加分支到if节点
ifNode.branches.push(branch)
// 替换当前节点为if节点
context.replaceNode(ifNode)
// 处理代码生成
processCodegen(ifNode, branch, true)
}v-for指令编译
// packages/compiler-core/src/directives/vFor.ts
/**
* 编译v-for指令
* @param node 当前节点
* @param dir 指令对象
* @param context 编译上下文
* @returns 编译结果
*/
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
// 解析v-for表达式,如"item in items"
const parseResult = parseForExpression(dir.exp!.content)
if (!parseResult) {
context.onError(createCompilerError(ErrorCodes.X_INVALID_FOR_DIRECTIVE_EXPRESSION, dir.loc))
return
}
const { source, value, key, index } = parseResult
// 创建for节点
const forNode: ForNode = {
type: NodeTypes.FOR,
source: dir.exp,
valueAlias: value,
keyAlias: key,
indexAlias: index,
children: [node],
loc: node.loc
}
// 替换当前节点为for节点
context.replaceNode(forNode)
// 处理代码生成
// ...
}
)3. 指令运行时
指令运行时的核心逻辑在runtime-core/src/directives/目录下,负责在运行时执行指令逻辑。
指令钩子类型
指令支持以下钩子函数,定义在runtime-core/src/directives/index.ts中:
// packages/runtime-core/src/directives/index.ts
/**
* 指令钩子类型
*/
export interface ObjectDirective<T = any, V = any> {
created?: DirectiveHook<T, null, V>
beforeMount?: DirectiveHook<T, null, V>
mounted?: DirectiveHook<T, null, V>
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
updated?: DirectiveHook<T, VNode<any, T>, V>
beforeUnmount?: DirectiveHook<T, null, V>
unmounted?: DirectiveHook<T, null, V>
getSSRProps?: SSRDirectiveHook
}
/**
* 指令钩子函数类型
*/
export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
el: T,
binding: DirectiveBinding<V>,
vnode: VNode<any, T>,
prevVNode: Prev
) => void
/**
* 指令绑定对象
*/
export interface DirectiveBinding<V = any> {
instance: ComponentInternalInstance | null
value: V
oldValue: V | undefined
arg?: string
modifiers: DirectiveModifiers
dir: ObjectDirective
}指令执行器
指令执行器负责在适当的时机调用指令钩子,定义在runtime-core/src/directives/index.ts中:
// packages/runtime-core/src/directives/index.ts
/**
* 执行指令钩子
* @param instance 组件实例
* @param vnode 虚拟节点
* @param prevVNode 旧虚拟节点
* @param name 指令名称
* @param hook 钩子类型
*/
export function invokeDirectiveHook(
instance: ComponentInternalInstance,
vnode: VNode,
prevVNode: VNode | null,
name: keyof ObjectDirective,
hook: Function
) {
const bindings = vnode.dirs!
const oldBindings = prevVNode && prevVNode.dirs!
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
const oldBinding = oldBindings && oldBindings[i]
// 调用指令钩子
hook(
binding.instance || instance,
vnode.el,
binding,
vnode,
oldVNode,
oldBinding
)
}
}4. 内置指令实现
v-model指令实现
v-model是Vue中最常用的指令之一,用于实现双向数据绑定。其核心实现如下:
// packages/runtime-core/src/directives/vModel.ts
/**
* v-model指令实现
*/
export const vModel: ObjectDirective = {
created(el, { value, modifiers: { lazy, number, trim } }, vnode) {
// 获取组件实例
const instance = vnode.context as ComponentInternalInstance
// 创建value ref
const valueRef = toRef(value)
// 根据元素类型处理v-model
if (isInputType(el, ['text', 'textarea'])) {
// 文本输入框处理
el.value = valueRef.value
// 监听输入事件
const event = lazy ? 'change' : 'input'
el.addEventListener(event, () => {
let newValue = el.value
// 处理trim修饰符
if (trim) {
newValue = newValue.trim()
}
// 处理number修饰符
if (number) {
newValue = parseFloat(newValue)
}
// 更新值
valueRef.value = newValue
})
} else if (isInputType(el, ['checkbox'])) {
// 复选框处理
// ...
} else if (isInputType(el, ['radio'])) {
// 单选框处理
// ...
} else if (isSelect(el)) {
// 选择框处理
// ...
}
},
// 其他钩子实现...
}v-show指令实现
v-show指令用于根据表达式的值切换元素的显示/隐藏状态:
// packages/runtime-core/src/directives/vShow.ts
/**
* v-show指令实现
*/
export const vShow: ObjectDirective = {
beforeMount(el, { value }) {
// 初始化显示状态
el._vod = el.style.display === 'none' ? '' : el.style.display
setDisplay(el, value)
},
mounted(el, { value }) {
// 挂载后确保显示状态正确
setDisplay(el, value)
},
updated(el, { value, oldValue }) {
// 更新时切换显示状态
if (value !== oldValue) {
setDisplay(el, value)
}
},
beforeUnmount(el) {
// 卸载前清理
const transition = el._vtc
if (transition) {
transition.stop()
}
}
}
/**
* 设置元素显示状态
* @param el 元素
* @param value 是否显示
*/
function setDisplay(el: any, value: any) {
el.style.display = value ? el._vod || '' : 'none'
}5. 自定义指令支持
Vue 3支持开发者自定义指令,其实现机制如下:
自定义指令注册
// packages/runtime-core/src/apiDirective.ts
/**
* 注册自定义指令
* @param name 指令名称
* @param directive 指令对象
*/
export function directive(name: string, directive?: Directive) {
// 如果没有提供指令对象,返回已注册的指令
if (!directive) {
return this._context.directives[name]
}
// 标准化指令
const normalizedDirective = normalizeDirective(directive)
// 注册指令到应用上下文
this._context.directives[name] = normalizedDirective
return this
}自定义指令运行时处理
在组件渲染过程中,会处理自定义指令:
// packages/runtime-core/src/component.ts
/**
* 处理组件指令
* @param instance 组件实例
* @param vnode 虚拟节点
*/
function invokeDirectiveHooks(
instance: ComponentInternalInstance,
vnode: VNode,
prevVNode: VNode | null,
hookName: keyof ObjectDirective
) {
const dirs = vnode.dirs
if (!dirs) return
for (let i = 0; i < dirs.length; i++) {
const dir = dirs[i]
const hook = dir.dir[hookName]
if (hook) {
try {
// 调用指令钩子
hook(
vnode.el,
dir,
vnode,
prevVNode
)
} catch (e: any) {
handleError(e, instance, ErrorCodes.DIRECTIVE_HOOK)
}
}
}
}6. 指令编译流程
指令编译是将模板中的指令转换为渲染函数的过程,主要包括以下步骤:
- 模板解析:在
parse.ts中识别指令,创建指令节点 - 指令转换:在
transform.ts中转换指令,生成代码生成节点 - 代码生成:在
codegen.ts中生成指令相关的渲染代码
例如,对于v-if指令的编译:
<div v-if="show">Hello</div>编译后生成的渲染函数:
function render(_ctx, _cache) {
return _ctx.show
? h('div', null, 'Hello')
: null
}指令的生命周期
指令具有以下生命周期钩子,与组件生命周期相对应:
| 钩子名称 | 调用时机 | 参数 |
|---|---|---|
created |
指令绑定到元素后,组件创建前 | el, binding, vnode |
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 |
自定义指令示例
下面是一个自定义指令的示例,用于实现元素自动聚焦:
// 注册自定义指令
app.directive('focus', {
// 当被绑定的元素挂载到DOM时执行
mounted(el) {
// 聚焦元素
el.focus()
},
// 当组件更新时执行
updated(el) {
// 再次聚焦元素
el.focus()
}
})
// 在模板中使用
<input v-focus>性能优化建议
- 避免过度使用指令:指令会增加编译和运行时开销,尽量使用组件代替复杂指令
- 合理使用指令钩子:只在必要的钩子中执行逻辑,避免在更新钩子中做过多计算
- 优化指令逻辑:指令逻辑要简洁高效,避免阻塞主线程
- 使用v-memo优化:对于频繁更新的指令,可以结合v-memo使用
- 避免在指令中操作DOM:尽量通过Vue的响应式系统更新DOM,而不是直接操作
总结
本集深入剖析了Vue 3指令系统的源码实现,包括指令的定义、注册、编译和运行时等核心模块。Vue 3指令系统设计灵活,支持内置指令和自定义指令,能够满足各种复杂的DOM操作需求。
理解指令系统的实现机制对于掌握Vue 3模板编译和运行时至关重要。通过合理使用指令,我们可以编写出更简洁、高效的Vue模板代码。
在后续的源码解析系列中,我们将继续深入探讨Vue 3的其他核心模块,包括插槽实现机制、异步组件等。