Vue 指令系统实现原理
深入解析 Vue 指令的注册、解析、执行全流程,剖析 v-model、v-if、v-show 等核心指令的底层实现机制。
一、前言
指令(Directive)是 Vue 提供的特殊属性,带有 v- 前缀,用于在表达式的值改变时,将某些行为应用到 DOM 上。从 v-if 的条件渲染到 v-model 的双向绑定,指令系统贯穿 Vue 开发的方方面面。
理解指令系统的实现原理,不仅能帮助我们更好地使用内置指令,更能指导我们编写出高效、可复用的自定义指令。本文将从源码层面剖析 Vue 指令系统的完整工作机制。
二、核心内容
2.1 指令注册机制
Vue 的指令分为内置指令和自定义指令。内置指令在框架初始化时注册,自定义指令通过 Vue.directive()(Vue2)或 app.directive()(Vue3)注册。
Vue2 指令注册
// 全局注册自定义指令
Vue.directive('focus', {
inserted(el) {
el.focus();
}
});
// 局部注册
export default {
directives: {
focus: {
inserted(el) {
el.focus();
}
}
}
}
Vue2 的指令注册本质上是在 Vue 构造函数上维护一个 options.directives 对象:
// core/global-api/assets.js
export function initAssetRegisters(Vue) {
const ASSET_TYPES = ['component', 'directive', 'filter'];
ASSET_TYPES.forEach(type => {
Vue[type] = function(id, definition) {
if (!definition) {
return this.options[type + 's'][id];
} else {
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition };
}
this.options[type + 's'][id] = definition;
return definition;
}
};
});
}
Vue3 指令注册
// Vue3 全局注册
const app = createApp(App);
app.directive('focus', {
mounted(el) {
el.focus();
}
});
// Vue3 局部注册
export default {
directives: {
focus: {
mounted(el) {
el.focus();
}
}
}
}
2.2 指令解析过程
指令的解析发生在编译阶段。编译器会识别模板中的 v- 属性,将其转换为渲染函数中的指令对象。
Vue2 指令解析
// 模板中的指令
// <div v-if="show" v-model="value" v-custom:arg.mod="expr"></div>
// 编译后生成的渲染函数大致结构
function render() {
with(this) {
return _c('div', {
directives: [
{ name: "custom", rawName: "v-custom:arg.mod", value: (expr), expression: "expr", arg: "arg", modifiers: {"mod": true} }
]
});
}
}
Vue2 的编译器在 processElement 阶段处理指令:
// compiler/parser/index.js
function processElement(element, options) {
processKey(element);
processRef(element);
// 处理指令
processAttrs(element);
return element;
}
function processAttrs(el) {
const list = el.attrsList;
for (let i = 0, l = list.length; i < l; i++) {
const name = list[i].name;
const value = list[i].value;
// 检测 v- 前缀
if (dirRE.test(name)) {
// 标记元素有指令
el.hasBindings = true;
// 解析指令名、参数、修饰符
const argMatch = name.match(argRE);
const arg = argMatch && argMatch[1];
// 添加指令到元素描述
addDirective(el, name, rawName, value, arg);
}
}
}
Vue3 指令解析
Vue3 的编译器对指令解析进行了重构,指令在 AST 转换阶段处理:
// compiler-core/transforms/vModel.ts
export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir;
if (!exp) {
context.onError(createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION));
}
// 生成指令运行时参数
const props = [
// modelValue: exp
createObjectProperty(
'modelValue',
createCompoundExpression([`$event`])
),
// 'onUpdate:modelValue': $event => exp = $event
createObjectProperty(
'onUpdate:modelValue',
createCompoundExpression([`$event => ((`, exp, `) = $event)`])
)
];
return createDirectiveArgument(props);
};
2.3 指令钩子函数
指令对象包含一组钩子函数,在元素的不同生命周期阶段调用。
Vue2 指令钩子
| 钩子 | 调用时机 |
|---|---|
bind | 指令第一次绑定到元素时 |
inserted | 被绑定元素插入父节点时 |
update | 组件更新时(子组件可能未更新) |
componentUpdated | 组件及子组件全部更新后 |
unbind | 指令与元素解绑时 |
Vue.directive('demo', {
bind(el, binding, vnode) {
// 初始化设置
el.style.color = binding.value;
},
inserted(el, binding, vnode) {
// DOM 插入后的操作
console.log('元素已插入:', el.parentNode);
},
update(el, binding, vnode, oldVnode) {
// 更新时
if (binding.value !== binding.oldValue) {
el.style.color = binding.value;
}
},
componentUpdated(el, binding, vnode, oldVnode) {
// 全部更新完成
},
unbind(el, binding, vnode) {
// 清理工作
}
});
Vue3 指令钩子
| 钩子 | 调用时机 |
|---|---|
created | 指令绑定到元素之前 |
beforeMount | 元素挂载前 |
mounted | 元素挂载后 |
beforeUpdate | 组件更新前 |
updated | 组件更新后 |
beforeUnmount | 元素卸载前 |
unmounted | 元素卸载后 |
const myDirective = {
created(el, binding, vnode, prevVnode) {
// 创建时的逻辑
},
beforeMount(el, binding, vnode, prevVnode) {
// 挂载前
},
mounted(el, binding, vnode, prevVnode) {
// 挂载后
},
beforeUpdate(el, binding, vnode, prevVnode) {
// 更新前
},
updated(el, binding, vnode, prevVnode) {
// 更新后
},
beforeUnmount(el, binding, vnode, prevVnode) {
// 卸载前清理
},
unmounted(el, binding, vnode, prevVnode) {
// 卸载完成
}
}
钩子参数详解
// binding 对象结构
{
arg: 'foo', // 指令参数 v-directive:foo
dir: { /* 指令定义对象 */ },
instance: component, // 组件实例
modifiers: { bar: true }, // 修饰符 v-directive:foo.bar
oldArg: ..., // 之前的参数(仅在 update 钩子中)
oldValue: ..., // 之前的值(仅在 update 钩子中)
value: 'baz' // 指令值 v-directive="'baz'"
}
2.4 v-model 实现原理
v-model 是 Vue 中最复杂的内置指令,它实现了表单元素与数据之间的双向绑定。
Vue2 v-model 实现
// 模板
// <input v-model="message">
// 编译后等效于
// <input :value="message" @input="message = $event.target.value">
// compiler/codegen/index.js
function genDirectives(el, state) {
const dirs = el.directives;
if (!dirs) return;
let res = 'directives:[';
let hasRuntime = false;
let needRuntime = false;
for (let i = 0, l = dirs.length; i < l; i++) {
const dir = dirs[i];
needRuntime = true;
const gen = state.directives[dir.name];
if (gen) {
// 编译时处理
hasRuntime = true;
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${dir.arg ? `,arg:${dir.arg}` : ''}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`;
}
}
return res + ']';
}
// 运行时 v-model 指令实现
const model = {
inserted(el, binding, vnode) {
if (vnode.tag === 'select') {
setSelected(el, binding, vnode.context);
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers;
if (!binding.modifiers.lazy) {
// 处理输入法组合事件
el.addEventListener('compositionstart', onCompositionStart);
el.addEventListener('compositionend', onCompositionEnd);
// 监听 input 事件
el.addEventListener('input', onInput);
}
}
},
// 组件的 v-model
componentUpdated(el, binding, vnode) {
if (vnode.tag === 'select') {
setSelected(el, binding, vnode.context);
}
}
};
Vue3 v-model 实现
Vue3 对 v-model 进行了重大改进,支持多个 v-model、自定义修饰符和自定义 modelValue 名称。
// compiler-core/transforms/vModel.ts
export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir;
// 处理参数(自定义 modelValue 名称)
const rawExp = exp.loc.source;
const eventName = arg
? `onUpdate:${arg.content}`
: 'onUpdate:modelValue';
// 生成 props
const props = [
createObjectProperty(
arg ? arg.content : 'modelValue',
createCompoundExpression([rawExp])
),
createObjectProperty(
eventName,
createCompoundExpression([
'$event => ((',
rawExp,
') = $event)'
])
)
];
// 处理修饰符
if (dir.modifiers) {
const modifiers = dir.modifiers;
// .trim, .number, .lazy 等修饰符处理
if (modifiers.trim) {
// 添加 trim 处理
}
if (modifiers.number) {
// 添加 number 转换
}
}
return {
props,
needRuntime: false
};
};
组件上的 v-model 实现:
<!-- 父组件 -->
<template>
<Child v-model:title="pageTitle" v-model="content" />
</template>
<!-- 子组件 -->
<template>
<input
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script>
export default {
props: ['title', 'modelValue'],
emits: ['update:title', 'update:modelValue']
}
</script>
2.5 v-if 与 v-show 实现差异
v-if 和 v-show 虽然都控制元素的显示/隐藏,但实现机制完全不同。
v-if 实现原理
v-if 是"真正的"条件渲染,它会根据条件销毁和重建元素及组件:
// 编译器将 v-if 转换为三元表达式
// <div v-if="ok">Yes</div>
// <div v-else>No</div>
// 编译结果
function render() {
with(this) {
return ok
? _c('div', [_v("Yes")])
: _c('div', [_v("No")]);
}
}
Vue3 中 v-if 的编译处理:
// compiler-core/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// 创建条件分支节点
return () => {
if (isRoot) {
// 根条件节点
ifNode.codegenNode = createConditionalExpression(
branch.condition,
branch.children,
createConstantExpression(false)
);
}
};
});
}
);
v-show 实现原理
v-show 通过 CSS display 属性控制显示,元素始终存在于 DOM 中:
// platforms/web/runtime/directives/show.js
const vShow = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display;
if (transition && value) {
transition.beforeEnter(el);
} else {
setDisplay(el, value);
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el);
}
},
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue) return;
if (transition) {
if (value) {
transition.beforeEnter(el);
setDisplay(el, true);
transition.enter(el);
} else {
transition.leave(el, () => {
setDisplay(el, false);
});
}
} else {
setDisplay(el, value);
}
},
beforeUnmount(el, { value }) {
setDisplay(el, value);
}
};
function setDisplay(el, value) {
el.style.display = value ? el._vod : 'none';
}
两者对比
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件渲染(销毁/重建) | CSS 切换(display) |
| 初始渲染开销 | 较高(需要编译条件分支) | 较低(始终渲染) |
| 切换开销 | 较高(组件销毁重建) | 较低(仅 CSS 切换) |
| 适用场景 | 运行时条件很少改变 | 需要频繁切换显示状态 |
| 与 transition 配合 | 进入/离开过渡 | 显示/隐藏过渡 |
2.6 自定义指令实现
自定义指令是扩展 Vue 功能的重要手段。下面通过几个实用示例展示自定义指令的实现。
示例 1:权限控制指令
// directives/permission.js
import { useUserStore } from '@/stores/user';
export const vPermission = {
mounted(el, binding) {
const { value } = binding;
const userStore = useUserStore();
const permissions = userStore.permissions;
if (value && value instanceof Array) {
const hasPermission = permissions.some(p => value.includes(p));
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
}
}
};
// 使用
// <button v-permission="['admin', 'editor']">管理按钮</button>
示例 2:防抖指令
// directives/debounce.js
export const vDebounce = {
mounted(el, binding) {
const { value, arg } = binding;
const delay = parseInt(arg) || 300;
let timer = null;
el._debounceHandler = (e) => {
clearTimeout(timer);
timer = setTimeout(() => {
value(e);
}, delay);
};
el.addEventListener('input', el._debounceHandler);
},
unmounted(el) {
el.removeEventListener('input', el._debounceHandler);
el._debounceHandler = null;
}
};
// 使用
// <input v-debounce:500="handleInput" />
示例 3:拖拽指令
// directives/draggable.js
export const vDraggable = {
mounted(el, binding) {
const { value = {} } = binding;
const { onDrag, onDragEnd, onDragStart } = value;
let isDragging = false;
let startX, startY, initialLeft, initialTop;
el.style.position = 'absolute';
el.style.cursor = 'move';
const onMouseDown = (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialLeft = el.offsetLeft;
initialTop = el.offsetTop;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
onDragStart && onDragStart({
left: initialLeft,
top: initialTop
});
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
el.style.left = `${initialLeft + dx}px`;
el.style.top = `${initialTop + dy}px`;
onDrag && onDrag({
left: initialLeft + dx,
top: initialTop + dy,
dx,
dy
});
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
onDragEnd && onDragEnd({
left: parseInt(el.style.left),
top: parseInt(el.style.top)
});
};
el.addEventListener('mousedown', onMouseDown);
// 存储清理函数
el._draggableCleanup = () => {
el.removeEventListener('mousedown', onMouseDown);
};
},
unmounted(el) {
el._draggableCleanup && el._draggableCleanup();
}
};
// 使用
// <div v-draggable="{ onDragEnd: handleDragEnd }">拖拽我</div>
三、Mermaid 图表
指令系统整体架构
v-model 双向绑定流程
四、代码示例
示例 1:完整的自定义指令注册与使用
// main.js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
// 全局注册自定义指令
app.directive('highlight', {
mounted(el, binding) {
const color = binding.value || 'yellow';
el.style.backgroundColor = color;
el.style.transition = 'background-color 0.3s';
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
el.style.backgroundColor = binding.value || 'yellow';
}
}
});
// 点击外部关闭指令
app.directive('click-outside', {
mounted(el, binding) {
el._clickOutside = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event);
}
};
document.addEventListener('click', el._clickOutside);
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside);
el._clickOutside = null;
}
});
app.mount('#app');
<!-- App.vue -->
<template>
<div>
<!-- 高亮指令 -->
<p v-highlight="highlightColor">这段文字会被高亮显示</p>
<button @click="changeColor">切换颜色</button>
<!-- 点击外部关闭 -->
<div v-click-outside="closeDropdown" class="dropdown">
<button @click="toggleDropdown">下拉菜单</button>
<div v-show="isOpen" class="dropdown-menu">
<div>选项 1</div>
<div>选项 2</div>
<div>选项 3</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const highlightColor = ref('yellow');
const isOpen = ref(false);
const changeColor = () => {
highlightColor.value = highlightColor.value === 'yellow' ? 'lightgreen' : 'yellow';
};
const toggleDropdown = () => {
isOpen.value = !isOpen.value;
};
const closeDropdown = () => {
isOpen.value = false;
};
return {
highlightColor,
isOpen,
changeColor,
toggleDropdown,
closeDropdown
};
}
};
</script>
<style>
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
border: 1px solid #ddd;
background: white;
padding: 10px;
min-width: 120px;
}
</style>
示例 2:v-model 自定义组件实现
<!-- CustomInput.vue -->
<template>
<div class="custom-input">
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
@blur="$emit('blur')"
/>
<span class="clear-btn" @click="clear" v-show="modelValue">×</span>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: {
modelValue: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'blur'],
setup(props, { emit }) {
const clear = () => {
emit('update:modelValue', '');
};
return { clear };
}
};
</script>
<style scoped>
.custom-input {
position: relative;
display: inline-block;
}
.custom-input input {
padding-right: 24px;
}
.clear-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #999;
}
</style>
<!-- Parent.vue -->
<template>
<div>
<CustomInput v-model="text" @blur="handleBlur" />
<p>当前值: {{ text }}</p>
</div>
</template>
<script>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
export default {
components: { CustomInput },
setup() {
const text = ref('');
const handleBlur = () => {
console.log('输入框失去焦点');
};
return { text, handleBlur };
}
};
</script>
示例 3:指令与 Composition API 结合
// composables/useDirective.js
import { getCurrentInstance, onMounted, onUnmounted } from 'vue';
/**
* 在组合式函数中创建指令效果
*/
export function useEventListener(target, event, handler) {
onMounted(() => {
target.addEventListener(event, handler);
});
onUnmounted(() => {
target.removeEventListener(event, handler);
});
}
/**
* 自动聚焦的组合式函数
*/
export function useFocus(elRef) {
onMounted(() => {
elRef.value?.focus();
});
}
/**
* 滚动加载更多
*/
export function useInfiniteScroll(elRef, callback, options = {}) {
const { threshold = 100, immediate = false } = options;
let isLoading = false;
const handler = async () => {
const el = elRef.value;
if (!el || isLoading) return;
const scrollBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
if (scrollBottom < threshold) {
isLoading = true;
await callback();
isLoading = false;
}
};
onMounted(() => {
const el = elRef.value;
if (el) {
el.addEventListener('scroll', handler);
if (immediate) handler();
}
});
onUnmounted(() => {
elRef.value?.removeEventListener('scroll', handler);
});
}
// 使用示例
import { ref } from 'vue';
import { useFocus, useInfiniteScroll } from './composables/useDirective.js';
export default {
setup() {
const inputRef = ref(null);
const listRef = ref(null);
const items = ref([]);
// 自动聚焦
useFocus(inputRef);
// 无限滚动
const loadMore = async () => {
const newItems = await fetchMoreItems();
items.value.push(...newItems);
};
useInfiniteScroll(listRef, loadMore, { immediate: true });
return {
inputRef,
listRef,
items
};
}
};
五、常见问题
Q1:自定义指令的钩子函数中如何访问组件实例?
在 Vue3 中,可以通过 binding.instance 访问组件实例。但需要注意,这不是响应式的,且不应该在指令中过度依赖组件实例的状态。
const directive = {
mounted(el, binding) {
const instance = binding.instance;
console.log('组件实例:', instance);
// 可以访问组件的 methods 或 data
instance.someMethod();
}
};
Q2:v-if 和 v-show 应该如何选择?
- 使用
v-if:当条件不常改变,或需要条件性渲染大量内容时。v-if在条件为 false 时不渲染元素,初始渲染开销较小。 - 使用
v-show:当需要频繁切换显示状态时。v-show只是切换 CSS display 属性,切换开销更小。
Q3:Vue3 中为什么移除了 bind 和 unbind 钩子?
Vue3 将指令钩子与组件生命周期对齐,使用 beforeMount/mounted 替代 bind/inserted,使用 beforeUnmount/unmounted 替代 unbind。这样命名更加一致,也更符合指令在组件生命周期中的实际执行时机。
Q4:如何在同一元素上使用多个 v-model?
Vue3 支持在同一组件上使用多个 v-model,通过参数区分:
<template>
<UserForm
v-model:name="user.name"
v-model:email="user.email"
v-model:age="user.age"
/>
</template>
Q5:自定义指令的修饰符如何工作?
修饰符在 binding.modifiers 中以对象形式提供:
// v-debounce:500.trim
const directive = {
mounted(el, binding) {
console.log(binding.modifiers); // { trim: true }
console.log(binding.arg); // "500"
}
};
六、总结
Vue 的指令系统是一个精心设计的扩展机制,它将 DOM 操作与响应式数据绑定解耦,让开发者可以用声明式的方式操作视图。
- 注册机制:指令通过全局或局部方式注册,在编译阶段被识别并转换为渲染函数的一部分
- 解析过程:编译器将
v-属性解析为指令对象,包含名称、参数、修饰符和表达式 - 钩子执行:指令在元素的生命周期各阶段执行对应的钩子函数,实现细粒度的 DOM 控制
- 内置指令:
v-model实现双向绑定的核心机制,v-if和v-show分别采用条件渲染和 CSS 切换策略 - 自定义扩展:通过自定义指令可以封装可复用的 DOM 操作逻辑,与 Composition API 结合使用效果更佳
掌握指令系统的实现原理,是成为 Vue 高级开发者的重要一步。
七、思考题
-
分析 Vue3 中
v-model的编译输出,对比.lazy、.number、.trim三个修饰符的实现差异。 -
实现一个
v-resize指令,监听元素尺寸变化并在变化时调用绑定函数。考虑使用 ResizeObserver API。 -
为什么
v-for的优先级高于v-if?如果反过来会有什么影响?查看源码分析这一设计决策。 -
在 Vue3 中,如何实现一个支持动画的
v-show自定义版本?参考内置v-show与transition组件的交互方式。
644

被折叠的 条评论
为什么被折叠?



