HOW - Vue 指令 - 内置指令底层实现和自定义指令

一、介绍

在Vue.js中,指令(Directives)是特殊的标记,添加在元素上的特殊属性,用来对 DOM 进行扩展。Vue自带一些内置指令,如v-bindv-modelv-forv-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成为一个强大且易用的前端框架。

  • 34
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值