目录
一、介绍
在Vue.js中,指令(Directives)是特殊的标记,添加在元素上的特殊属性,用来对 DOM
进行扩展。Vue自带一些内置指令,如v-bind
、v-model
、v-for
、v-if
等,开发者也可以创建自定义指令。
二、内置指令
最新源码参考:https://github.com/vuejs/vue/tree/main/src/compiler/directives
1. v-bind
基本使用
用于绑定元素的属性。
例子:
<a v-bind:href="url">Link</a>
底层简单实现
https://github.com/vuejs/vue/blob/main/src/compiler/directives/bind.ts
它的实现涉及解析绑定表达式并更新DOM属性。
export default function bind(el, dir) {
el.props = el.props || [];
el.props.push({
name: dir.name,
value: dir.value,
dynamic: !!dir.dynamic
})
}
2. v-model
基本使用
用于在表单控件上创建双向数据绑定。
例子:
<input v-model="message">
<p>{{ message }}</p>
底层简单实现
https://github.com/vuejs/vue/blob/main/src/compiler/directives/model.ts
v-model 用于在表单控件上创建双向数据绑定。实现方式因不同控件类型而异,如 input、 textarea 和 select。
这里以 input 和 textarea 为例:
function parseBracket(chr) {
let inBracket = 1;
expressionPos = index;
while (!eof()) {
chr = next();
if (isStringStart(chr)) {
parseString(chr);
continue;
if (chr === 0x5b) inBracket++;
if (chr === 0x5d) inBracket--;
if (inBracket === 0) {
expressionEndPos = index;
break;
}
}
}
}
function parseString(chr) {
const stringQuote = chr;
while (!eof()) {
chr = next();
if (chr === stringQuote) {
break;
}
}
}
function isStringStart(chr) {
return chr === 0x22 || chr === 0x27; // 分别代表双引号 " 和单引号 '
}
function eof() {
return index >= len;
}
function next() {
return str.charCodeAt(++index);
}
function parseModel(val) {
// 1. 解析 v-model 绑定的值,确定表达式和键(如果有的话)
// 2. 需要处理普通变量、点号分隔的属性访问(如 obj.prop)和数组形式属性访问(如 obj[key])
val = val.trim();
const len = val.length;
if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {
const index = val.lastIndexOf('.');
const index = val.lastIndexOf('.')
if (index > -1) {
// 处理点号分隔的属性访问(如 obj.prop)
return {
exp: val.slice(0, index),
key: `"${val.slice(index + 1)}"`
}
} else {
return {
exp: val,
key: null,
}
}
}
// 处理数组访问(如 obj[key])
// 在处理数组形式属性访问时,可能会遇到字符串索引,这时需要解析字符串,以确保解析的正确性。如 obj['mykey']
const str = val;
let index = 0;
let expressionPos = 0;
let expressionEndPos = 0;
while (!eof()) {
const chr = next();
if (isStringStart(chr)) { // ", '
parseString(chr);
} else if (chr === 0x5b) { // [
parseBracket(chr);
}
}
return {
exp: val.slice(0, expressionPos),
key: val.slice(expressionPos + 1, expressionEndPos)
}
}
function genAssignmentCode(value, assignment) {
// 生成赋值代码,根据解析的结果生成赋值语句。如果有键,则使用 $set 方法进行赋值。
const res = parseModel(value);
if (res.key === null) {
return `${value}=${assignment}`
} else {
// $set
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
function genDefaultModel(el, value, modifiers) {
// 生成数据绑定和事件监听代码
const { number, trim } = modifiers || {};
let baseValueExpression = '$event.target.value';
if (trim) {
baseValueExpression = `$event.target.value.trim()`
}
if (number) {
baseValueExpression = `_n(${baseValueExpression})`
}
// 生成赋值代码
const assignment = genAssignmentCode(value, baseValueExpression);
// 将生成的值、表达式和回调函数保存到元素的 model 对象中。
el.model = {
value: `(${value})`,
expression: JSON.stringify(value),
callback: `function ($event) {${assignment}}`
}
}
export default function model(el, dir, _warn) {
// 根据元素类型选择相应的处理函数
const value = dir.value;
const tag = el.tag;
// 以 input 和 textarea 为例
if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers);
}
// 更多类型的处理...
}
3. v-for
用于基于一个数组渲染一个列表。
例子:
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
4. v-if, v-else-if, v-else
用于条件渲染。
例子:
<p v-if="isAdmin">Admin</p>
<p v-else-if="isModerator">Moderator</p>
<p v-else>User</p>
5. v-show
用于切换元素的可见性。
例子:
<p v-show="isVisible">This is visible</p>
6. v-on
基本使用
用于绑定事件监听器。
例子:
<button v-on:click="doSomething">Click me</button>
底层简单实现
https://github.com/vuejs/vue/blob/main/src/compiler/directives/on.ts
export default function on(el, dir) {
el.wrapListeners = (code: string) => `_g(${code},${dir.value})`;
}
7. v-cloak
用于保持元素在编译之前处于隐藏状态。
例子:
<style>
[v-cloak] { display: none; }
</style>
<div v-cloak>{{ message }}</div>
8. v-pre
用于跳过这个元素和它的子元素的编译过程。
例子:
<span v-pre>{{ this will not be compiled }}</span>
9. v-once
只渲染元素和组件一次。
例子:
<span v-once>{{ message }}</span>
三、自定义指令
3.1 介绍
Vue.js允许创建自定义指令以复用复杂的DOM操作逻辑。自定义指令可以在组件的 directives
选项中定义。
在 Vue 中有两种重用代码的方式:组件
和组合式函数
。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。而自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
注意,只有当所需功能只能通过直接的 DOM 操作
来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
在官方文档底部也提到过:
不推荐在组件上使用自定义指令,当组件具有多个根节点时可能会出现预期外的行为。当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传 attributes 类似。需要注意的是组件可能含有多个根节点。当应用到一个多根组件时,指令将会被忽略且抛出一个警告。和 attribute 不同,指令不能通过
v-bind="$attrs"
来传递给一个不同的元素。
3.1 注册自定义指令
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
全局注册:
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.directive('focus', {
mounted: function (el) {
el.focus();
}
});
局部注册:
export default {
directives: {
focus: {
mounted: function (el) {
el.focus();
}
}
}
}
v-focus
指令比 autofocus attribute 更有用,因为它不仅仅可以在页面加载完成后生效,还可以在 Vue 动态插入元素后生效。
3.2 使用自定义指令
例子:
<input v-focus>
3.3 自定义指令的钩子函数
自定义指令可以有以下钩子函数:
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
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) {}
}
关于自定义指令更多实践将在下一篇阐述。
四、总结
Vue.js 提供了丰富的内置指令,能够帮助开发者轻松完成常见的DOM操作和数据绑定需求。
通过自定义指令,开发者还可以扩展Vue.js的功能,以适应更复杂的场景和需求。
这种灵活性使得Vue.js成为一个强大且易用的前端框架。