1. 自定义指令重点提炼
自定义指令
- 官网:v-focus
- 拖拽:v-drag
- Vue.directive()
- 生命周期(钩子函数)
- bind
- update
- componentUpdated
- unbind
- el
- binding
2. Vue重要概念简述
组件:在应用开发当中能够复用的一些结构、逻辑、样式等内容、即应用(app
)的组织结构。
指令:用一种特殊标签属性来对当前渲染的组件(标签)进行功能扩展,影响当前标签在渲染过程中的一些行为。指令是用在标签上面的,但用起来比较特殊,采用v-
的形式作为前缀的,这样的一套属性,有着特殊的作用,vue在解析过程中,它会去影响当前指令所在的标签,影响它的结构、渲染行为、表现等。
我们还可以通过 Vue
提供的方法来自定义指令
3. 注册指令
和注册组件
的概念一样
vue
提供了两种指令注册方式
- 全局指令:在应用任何位置都可以使用
- 局部指令:注册在哪个组件,仅限该组件内部使用
4. 全局指令
Vue.directive('指令名称', {指令配置});
通过directive
方法注册指令,重点在于指令配置,它实际提供了一套配置,我们为该配置传入不同的配置参数,它就可以自动生成我们想要的东西了,从而影响整个应用的一些行为。
它也有点类似组件, 这里的配置称为指令生命周期(钩子函数)
当一个指令作用于页面当中某个元素或组件的时候,在该组件渲染过程当中,解析这个组件过程当中,发现有指令,就会解析到指令上面来,接接着进入指令生命周期,执行这些钩子函数。
我们引入一个官网的例子,实现元素的焦点行为 => 在页面当中有一个input输入框,这个输入框可以通过某种简便地方式,给其设置焦点。往下看 =>
4.1 example01
实现设置焦点的例子。
4.1.1 example01-1
原生js
是通过dom
获取元素设置一个focus
方法即可,但在vue
当中尽量不要操作dom
。
我们使用autofocus
属性(autofocus
属性规定当页面加载时input
元素应该自动获得焦点。
如果使用该属性,则input
元素会获得焦点。),但是它的弊端是后期我们不能自定义控制焦点。
而我们的需求是 => 一刷新页面后,焦点不要自动在input
输入框上,而是点击按钮后,焦点才会在上面。因此通过属性绑定方式,是无法实现该种需求的,因此还是需要操作dom
的。
如何获取input
呢?
=> 使用原生js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" autofocus />
<button @click="setFocus">设置焦点</button>
</div>
<script src="./js/vue.js"></script>
<script>
let app = new Vue({
el: '#app',
methods: {
setFocus() {
let input = document.querySelector('input');
console.log(input);
input.focus();
}
}
});
</script>
</body>
</html>
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.57
Branch: branch02commit description:a0.57(example01-1——原生js获取焦点)
tag:a0.57
注意:vue不是不允许操作dom,还是建议尽量不使用dom操作,能避免就避免。如果dom
操作与数据有关,数据变化会影响dom
结构的话,尽量用数据驱动视图变化,但是并不代表所有的东西都可以用数据来驱动,可能用数据驱动会很繁琐,所以有时也需要用dom
。
4.1.2 example01-2
以上用原生获取dom
会比较麻烦,vue
提供更为简便的方式操作dom
。
=> 我们使用ref
来获取。
ref
:vue提供的一种用于获取标签(组件)实例对象的简便方式。
vue
对象下的$refs
对象属性在解析过程中会把当前html
模板当中的所有标有ref
的元素对应的实例对象(如原生dom
对象)全部存在它下面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- ref的值随便取,其实就相当于id -->
<input ref="input1" type="text" autofocus />
<button @click="setFocus">设置焦点</button>
</div>
<script src="./js/vue.js"></script>
<script>
let app = new Vue({
el: '#app',
methods: {
setFocus() {
console.log(this.$refs)
this.$refs.input1.focus();
}
}
});
</script>
</body>
</html>
$refs
该对象的key
就是我们在标签中设置的ref
的值。
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.58
Branch: branch02commit description:a0.58(example01-2——vue简便快捷方式获取焦点)
tag:a0.58
其实越偏底层,自己去写自定义组件的时候,封装一些UI组件的时候,经常会用到它。
除了这种方法,还有其他方式实现 => 自定义指令
4.2 自定义指令实例
参看指令生命周期与官网例子下的example02
。
5. 局部指令
new Vue({
el: '#app',
directives: {
'指令名称': {指令配置}
}
});
在使用指令的时候(指令定义的时候不需要使用 v-),需要使用
v-指令名称
的方式来调用
6. 指令生命周期(钩子函数)
和组件生命周期(钩子函数)一样的,当一个指令作用于页面当中一个元素(组件)的时候,那么在该组件渲染的过程当中,在解析这个组件(渲染的这个标签)的过程中,它会解析到这个指令(vue
指令)上面来,它会执行后面传入的钩子函数。一共有5个钩子函数,bind、inserted、update、componentUpdated、unbind 。
指令的运行方式很简单,它提供了一组指令生命周期钩子函数,我们只需要在不同的生命周期钩子函数中进行逻辑处理就可以了。
在不同的生命周期,都可以接收一些参数,具体参见传入参数
。
6.1 bind
bind : 只调用一次(和组件初始化一样),指令第一次绑定到元素时(解析的一瞬间)调用。在这里可以进行一次性的初始化设置。
解析过程中第一次,有点类似document.createElement()
。虽然创建了,但是在dom树
当中是不存在的。就相当于这个组件已经被变成对象了,就相当于它被构建出来了,但是在页面dom树
中是不存在的。
这个时候比如进行获取它的父节点,获取子节点的时候可能会出一些问题。
6.2 inserted
inserted : 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中),与bind两者之间有一个很大的区别。
当前的组件被插入dom
元素的时候调用,代表当前的元素已经被添加到dom树
当中,比如调用appendChild
,insertBefore
行为,此时就可以操作dom树
了。
比如进行获取它的父节点、获取子节点、获取宽高等操作。
但是是不是一定可以获取到呢?
比如宽高,那不一定,得看具体的行为,比如这个元素要是隐藏的呢!这样的话,宽高一样获取不到,但是它已经表示在dom结构
当中是可以的了。
6.3 update
update : 所在组件更新的时候调用
6.4 componentUpdated
componentUpdated : 所在组件更新完成后调用
6.5 unbind
unbind : 只调用一次,指令与元素解绑时调用
6.6 传入参数
不同的生命周期钩子函数在调用的时候同时会接收到传入的一些不同的参数
- el : 指令所绑定的元素,可以用来直接操作
DOM
- binding : 一个对象,包含以下属性:
- name : 指令名,不包括
v-
前缀 - value : 指令的绑定值(作为表达式解析后的结果)
- expression : 指令绑定的表达式(字符串)
- arg : 传给指令的参数,可选
- modifiers : 传给指令的修饰符组成的对象,可选,每个修饰符对应一个布尔值
- oldValue : 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用,无论值是否改变都可用
- name : 指令名,不包括
7. 案例
7.1 官网的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-focus>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.directive('focus', {
inserted(el) {
el.focus();
}
});
let app = new Vue({
el: '#app'
});
</script>
</body>
</html>
7.2 example02
7.2.1 example02-1
我们先看看bind的使用
指令定义的时候不需要使用 v-
,但是调用的时候,是需要的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-focus />
<button @click="setFocus">设置焦点</button>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('focus', {
bind(el, binding) {
console.log('bind', el);
el.focus();
},
inserted(el, binding) {
console.log('inserted',el);
}
});
let app = new Vue({
el: '#app',
methods: {
setFocus() {
}
}
});
</script>
</body>
</html>
可是感觉没有任何效果。
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.59
Branch: branch02commit description:a0.59(example02-1——bind中获取焦点,但没有反应)
tag:a0.59
7.2.2 example02-2
bind(el, binding) {
console.log('bind', el);
// el.focus();
el.value = 'CCCCCCCCCCCCCCCCCCC';
},
inserted(el, binding) {
console.log('inserted');
}
});
发现有效果了。 但是focus
为什么没效果呢?
因为focus
有效果的前提是当前这个元素已经在dom树
中存在了。
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.60
Branch: branch02commit description:a0.60(example02-2——bind中设置input的value却有反应。)
tag:a0.60
7.2.3 example02-3
如原生代码,不能创建一个div我们就去使用,肯定不行。除非将元素加入到dom树当中才可以。
window.onload = function() {
let input = document.createElement('input');
input.focus();
document.body.appendChild(input);
}
window.onload = function() {
let input = document.createElement('input');
document.body.appendChild(input);
input.focus();
}
很明显,元素在页面当中存在的时候,focus才能生效。
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.61
Branch: branch02commit description:a0.61(example02-3——bind中focus生效原理,原生代码演示)
tag:a0.61
7.2.4 example02-4
有些操作是直接可以针对dom
对象进行的,而有些操作必须是基于当前在dom树
中存在的对象 => 如与宽、高打交道的一些操作等。
在bind操作的话,是不能保证当前元素已经在dom中存在了,因为还存在解析过程和渲染过程,而inserted 操作则代表当前元素已经在dom节点当中了。
bind(el, binding) {
console.log('bind', el);
// el.focus();
// el.value = 'CCCCCCCCCCCCCCCCCCC';
},
inserted(el, binding) {
console.log('inserted');
el.focus();
}
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.62
Branch: branch02commit description:a0.62(example02-4——insert中focus才能生效)
tag:a0.62
7.2.5 example02-5
然后我们想实现上来没有焦点,而是通过点击按钮的时候才有焦点=>可利用绑定值的方式来实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-focus="isFocus" />
<button @click="setFocus">设置焦点</button>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('focus', {
bind(el, binding) {
console.log('....');
},
inserted(el, binding) {
console.log(binding);
}
});
let app = new Vue({
el: '#app',
data: {
isFocus: false
},
methods: {
setFocus() {
}
}
});
</script>
</body>
</html>
通过binding参数可获取一个对象
value : 存储的是(v-focus="isFocus"
)指令的值(作为表达式解析过后的)
expression : 表达式原始内容(v-focus="isFocus"
)
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.63
Branch: branch02commit description:a0.63(example02-5——binding参数的使用)
tag:a0.63
7.2.6 example02-6
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-focus="isFocus" />
<button @click="setFocus">设置焦点</button>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('focus', {
bind(el, binding) {
console.log('....');
},
// {value}解构binding中的value属性
inserted(el, {value}) {
// value : 存储的是 指令的值(作为表达式解析过后的)
// expression : 表达式原始内容
// console.log(binding);
if (value) {
el.focus();
}
}
});
let app = new Vue({
el: '#app',
data: {
isFocus: false
},
methods: {
setFocus() {
}
}
});
</script>
</body>
</html>
希望当数据更新的时候,与数据关联的组件会重新渲染,在重新渲染过程中会看到v-focus
,那会不会重新调用bind
和inserted
呢?
显然是不会的。
指令和组件很相似,并不是指令重新渲染,它就重新销毁再创建的。
这个时候触发的是componentUpdated
Vue.directive('focus', {
bind(el, binding) {
console.log('....');
},
// {value}解构binding中的value属性
inserted(el, {value}) {
// value : 存储的是 指令的值(作为表达式解析过后的)
// expression : 表达式原始内容
// console.log(binding);
if (value) {
el.focus();
}
},
componentUpdated(el, binding) {
console.log(binding);
}
});
let app = new Vue({
el: '#app',
data: {
isFocus: false
},
methods: {
setFocus() {
this.isFocus = true;
}
}
});
新值value
变为true
,而在这里也显示原始值oldValue
为false
以后复用,直接绑定我们自定义的v-focus
指令就行了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-focus="isFocus" />
<button @click="setFocus">设置焦点</button>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('focus', {
bind(el, binding) {
console.log('....');
},
// {value}解构binding中的value属性
inserted(el, {value}) {
// value : 存储的是 指令的值(作为表达式解析过后的)
// expression : 表达式原始内容
// console.log(binding);
if (value) {
el.focus();
}
},
componentUpdated(el, {value}) {
// console.log(binding);
if (value) {
el.focus();
}
}
});
let app = new Vue({
el: '#app',
data: {
isFocus: false
},
methods: {
setFocus() {
this.isFocus = true;
}
}
});
</script>
</body>
</html>
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.64
Branch: branch02commit description:a0.64(example02-6——实现指令获取焦点行为的最终版)
tag:a0.64
7.3 扩展:自定义拖拽指令
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.box {
position: absolute;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="app">
<button @click="canDrag = !canDrag">Drag : {{canDrag}}</button>
<div class="box" v-drag.limit="canDrag"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.directive('drag', {
bind(el, {modifiers,value}) {
let isDragStart = false;
let disX = 0;
let disY = 0;
el.canDrag = value;
el.addEventListener('mousedown', e => {
if (!el.canDrag) return;
disX = e.clientX - el.offsetLeft;
disY = e.clientY - el.offsetTop;
isDragStart = true;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (isDragStart) {
let x = e.clientX - disX;
let y = e.clientY - disY;
if (modifiers.limit) {
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
}
el.style.left = x + 'px';
el.style.top = y + 'px';
}
});
document.addEventListener('mouseup', e => {
isDragStart = false;
});
},
componentUpdated(el, {value}) {
console.log('componentUpdated', value);
el.canDrag = value;
}
});
let app = new Vue({
el: '#app',
data: {
canDrag: false
}
});
</script>
</body>
</html>
7.4 example03
具体拖住原理,请参考小迪之前写的文章。
Event事件学习实用路线(10)——Event事件之拖拽原理思路详解
7.4.1 example03-1
注意:事件绑定是不需要元素在这个页面存在的,除非触发该事件,元素就必须在页面上了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.box {
position: absolute;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="app">
<div class="box" v-drag></div>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('drag', {
bind(el, binding) {
// 事件绑定是不需要元素在这个页面存在的,除非触发该事件,元素就必须在页面上了
let isDown = false; // 鼠标是否按下
let _x = 0;
let _y = 0;
el.addEventListener('mousedown', function(e) {
isDown = true;
// 鼠标与元素差值,便于计算拖拽后的位置
_x = e.clientX - el.offsetLeft;
_y = e.clientY - el.offsetTop;
e.stopPropagation(); // 阻止冒泡
e.preventDefault(); // 阻止默认行为
});
el.addEventListener('mousemove', function(e) {
if (isDown) {
el.style.left = e.clientX - _x + 'px';
el.style.top = e.clientY - _y + 'px';
}
});
el.addEventListener('mouseup', function() {
isDown = false;
});
}
});
let app = new Vue({
el: '#app'
});
</script>
</body>
</html>
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.65
Branch: branch02commit description:a0.65(example03-1——初步实现自定义拖拽指令)
tag:a0.65
7.4.2 example03-2
我们进行优化,绑定数据 => 是否允许拖拽,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.box {
position: absolute;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background: #e30505;
}
</style>
</head>
<body>
<div id="app">
<button @click="isDrag=!isDrag">开启拖拽 - {{isDrag}}</button>
<div class="box" v-drag="isDrag"></div>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('drag', {
bind(el, {value: isDrag}) {
let isDown = false;
let _x = 0;
let _y = 0;
el.addEventListener('mousedown', function(e) {
console.log(isDrag)
if (!isDrag) {
return;
}
isDown = true;
_x = e.clientX - el.offsetLeft;
_y = e.clientY - el.offsetTop;
e.stopPropagation();
e.preventDefault();
});
el.addEventListener('mousemove', function(e) {
if (isDown) {
el.style.left = e.clientX - _x + 'px';
el.style.top = e.clientY - _y + 'px';
}
});
el.addEventListener('mouseup', function() {
isDown = false;
});
}
});
let app = new Vue({
el: '#app',
data: {
// 是否允许拖拽
isDrag: false
}
});
</script>
</body>
</html>
bind
只能执行一次,而实际在函数中的isDrag
是从bind
取出来的,因此鼠标在元素身上按下的时候,它根据作用域链找到执行的bind
函数之后,去拿isDrag
,但它是初始化传进来的,永远都是false
(取不到最新值)。
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.66
Branch: branch02commit description:a0.66(example03-2——优化,绑定数据 => 是否允许拖拽,bind带来的问题)
tag:a0.66
7.4.3 example03-3
实际只要事件触发,update
就会执行。
如何在update
中更新bind
中参数值(它俩是不同的汉化)呢?
这就和数据传参组件传递一样,原生js
中最容易想到的就是利用全局变量
。
这样写容易造成混乱,因此我们可以将其挂载到对象
下,挂载都共同的父级,这里我们选择指令所绑定的共同元素对象el
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.box {
position: absolute;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background: #e30505;
}
</style>
</head>
<body>
<div id="app">
<button @click="isDrag=!isDrag">开启拖拽 - {{isDrag}}</button>
<div class="box" v-drag="isDrag"></div>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('drag', {
bind(el, {value: isDrag}) {
let isDown = false;
el._isDrag = isDrag;
let _x = 0;
let _y = 0;
el.addEventListener('mousedown', function(e) {
// console.log('downdowndown')
console.log(el._isDrag)
if (!el._isDrag) {
return;
}
isDown = true;
_x = e.clientX - el.offsetLeft;
_y = e.clientY - el.offsetTop;
e.stopPropagation();
e.preventDefault();
});
el.addEventListener('mousemove', function(e) {
if (isDown) {
el.style.left = e.clientX - _x + 'px';
el.style.top = e.clientY - _y + 'px';
}
});
el.addEventListener('mouseup', function() {
isDown = false;
});
},
update(el, {value: isDrag}) {
el._isDrag = isDrag;
console.log(el._isDrag)
}
});
let app = new Vue({
el: '#app',
data: {
// 是否允许拖拽
isDrag: false
}
});
</script>
</body>
</html>
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.67
Branch: branch02commit description:a0.67(example03-3——优化,绑定数据 => 是否允许拖拽,解决bind带来的问题)
tag:a0.67
7.4.4 example03-4
实现限制拖拽范围:
modifiers : 传给指令的修饰符组成的对象,可选,每个修饰符对应一个布尔值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.box {
position: absolute;
left: 100px;
top: 100px;
width: 100px;
height: 100px;
background: red;
}
.box2 {
position: absolute;
left: 300px;
top: 100px;
width: 100px;
height: 100px;
background: green;
}
</style>
</head>
<body>
<div id="app">
<button @click="isDrag=!isDrag">开启拖拽 - {{isDrag}}</button>
<div class="box" v-drag.limit="isDrag"></div>
<div class="box2" v-drag="isDrag"></div>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.directive('drag', {
bind(el, {value: isDrag, modifiers}) {
console.log(modifiers);
console.log(modifiers.limit);
let isDown = false;
el._isDrag = isDrag;
el._isLmit = modifiers.limit;
let _x = 0;
let _y = 0;
el.addEventListener('mousedown', function(e) {
// console.log('downdowndown')
// console.log(el._isDrag)
if (!el._isDrag) {
return;
}
isDown = true;
_x = e.clientX - el.offsetLeft;
_y = e.clientY - el.offsetTop;
e.stopPropagation();
e.preventDefault();
});
el.addEventListener('mousemove', function(e) {
if (isDown) {
let L = e.clientX - _x;
let T = e.clientY - _y;
if (el._isLmit) {
// 限制左侧
if (L < 0) {
L = 0;
}
}
el.style.left = L + 'px';
el.style.top = T + 'px';
}
});
el.addEventListener('mouseup', function() {
isDown = false;
});
},
update(el, {value: isDrag}) {
el._isDrag = isDrag;
// console.log(el._isDrag)
}
});
let app = new Vue({
el: '#app',
data: {
isDrag: false
}
});
</script>
</body>
</html>
参考:https://https://github.com/6xiaoDi/blog-vue-Novice/tree/a0.68
Branch: branch02commit description:a0.68(example03-4——实现限制拖拽范围-modifiers的使用)
tag:a0.68
同时可以实现一个自定义右键菜单指令,如给标签加一个v-contextmenu
指令,里面传一个数组 => v-contextmenu="items"
。
8. 小结-对比React
什么情况下去使用指令呢?
其实是期望对于某一种已存在的元素进行行为扩展的时候,不是结构扩展,结构扩展是用组件去实现的。
而在React
是没有这种指令存在的,那如果在React
中想实现拖拽,是怎么办到的呢?
可以使用容器组件,当然vue
中也可以通过这种形式实现
<drag>
<div class="box"></div>
</drag>
(后续待补充)