Vue之自定义指令
先了解一下,在 vue
中,有很多内置的指令.
比如:
-
v-for
用于遍历 -
v-if
&v-show
用于隐藏和显示元素(区别在于后者是修改display:block|none
,前者是不创建把元素从dom中删除或者创建. -
v-bind:
属性绑定,把数据绑定在HTML元素的属性上. -
v-html
&v-text
把数据绑定在HTML元素的属性上,作用同innerHTML
&innerText
-
v-on:
绑定HTML元素事件 -
v-if
&v-else-if
&v-else
条件渲染 -
v-model
绑定表单元素,实现双向绑定.
等等.....
所以,关于指令,我们可以总结下面几点:
- 指令是写在 HTML 属性地方的.
<input v-model='name' type='text' />
- 指令都是以
v-
开头的. - 指令表达式的右边一般也可以跟值
v-if = false
2.0 Vue自定义指令场景小DEMO(非常尬)
场景:
我们需要一个指令,写在某个HTML表单元素上,然后让它在被加载到DOM中时,自动获取焦点.
// 和自定义过滤器一样,我们这里定义的是全局指令
Vue.directive('focus',{
inserted(el) {
el.focus()
}
})
<div id='app'>
<input type="text">
<input type="text" v-focus placeholder="我有v-focus,所以,我获取了焦点">
</div>
这里放了两个 input
,但是后面的 input
才使用了我们的自定义 v-focus
指令,所以看到了是后面那个文本框获取了焦点,而不是前面一个.
先总结几个点:
- 使用
Vue.directive()
来新建一个全局指令,(指令使用在HTML元素属性上的) -
Vue.directive('focus')
第一个参数focus
是指令名,指令名在声明的时候,不需要加v-
- 在使用指令的HTML元素上,
<input type="text" v-focus placeholder="我有v-focus,所以,我获取了焦点"/>
我们需要加上v-
. -
Vue.directive('focus',{})
第二个参数是一个对象,对象内部有个inserted()
的函数,函数有el
这个参数.-
el
这个参数表示了绑定这个指令的DOM元素
,在这里就是后面那个有placeholder
的input
-
el
就等价于document.getElementById('el.id')
.... - 可以利用
$(el)
无缝连接jQuery
-
指令的生命周期
其实上面个例子很尬!
HTML5 本身就提供了一个 autofocus
的属性,我们直接写这个就OK了.
用指令我们需要:
- 新增一个指令
- 定义指令的第二个参数里的
inserted
函数 - 在需要获取焦点的元素上,使用这个指令.
非常的费时费力....
在说明,为什么我要这么麻烦的使用指令之前,可以先了解一下指令的一些基本知识.
上述例子中,我们写了一个叫 inserted(){}
的函数,
有了 inserted
就说明了,指令在绑定到 HTML 元素上时,肯定也是有一堆钩子函数的,说白了也就是生命周期.
当一个指令绑定到一个元素上时,其实指令的内部会有五个生命周期事件函数.
先上官方说明:
-
bind(){}
当指令绑定到 HTML 元素上时触发.只调用一次. -
inserted()
当绑定了指令的这个HTML元素插入到父元素上时触发(在这里父元素是div#app
).但不保证,父元素已经插入了 DOM 文档. -
updated()
所在组件的VNode
更新时调用. -
componentUpdate
指令所在的组件的VNode
以及其子VNode
全部更新后调用. -
unbind
: 指令和元素解绑的时候调用,只调用一次
Vue 指令的声明周期函数
Vue.directive('gqs',{
bind() {
// 当指令绑定到 HTML 元素上时触发.**只调用一次**
console.log('bind triggerd')
},
inserted() {
// 当绑定了指令的这个HTML元素插入到父元素上时触发(在这里父元素是 `div#app`)**.但不保证,父元素已经插入了 DOM 文档.**
console.log('inserted triggerd')
},
updated() {
// 所在组件的`VNode`更新时调用.
console.log('updated triggerd')
},
componentUpdated() {
// 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
console.log('componentUpdated triggerd')
},
unbind() {
// 只调用一次,指令与元素解绑时调用.
console.log('unbind triggerd')
}
})
HTML
<div id='app' v-gqs></div>
结果:
bind triggerd
inserted triggerd
发现默认情况下只有 bind
和 inserted
声明周期函数触发了.
那么剩下的三个什么时候触发呢?
<div id='app' >
<p v-gqs v-if="show">v-if是删除或者新建dom元素,它会触发unbind指令声明周期吗?</p>
<button @click="show=!show">toggle</button>
</div>
一开始猜测 unbind
应该是删除元素的时候触发,也是弄了个 v-if
.
然后点击按钮,发现果然如此.
当指令绑定的元素被销毁时,会触发指令的 unbind
事件.
(新建并显示,仍然是触发 bind
& inserted
)
<p v-gqs v-show="show2">v-show设置元素的display:block|none,会触发componentUpdated事件</p>
<button @click="show2=!show2">toggle-v-show</button>
componentUpdated.gif
unbind()
和 componentUpdated()
- 一个把元素从DOM删除触发
unbind()
.---> 仅仅是删除. - 一个显示设置元素的隐藏和显示的时候触发
componentUpdated()
---> block | none 都触发.
但是 updated()
声明周期什么时候触发呢?
官方解释是 :
所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
反正有点看不懂..
所在组件的VNode??? 指令不是写在HTML元素上的吗?这里的组件指的是绑定了指令的HTML元素吗???
尝试用一个 list
列表来试验...
fruits:[
'🍎',
'🍌',
'🍊',
'🍇',
'🍑'
]
<ul>
<li v-gqs v-for="(f,i) in fruits" :key="i">{{f}} <a href="#" @click.prevent='delF(i)'>X</a></li>
</ul>
updated测试1.gif
- 仍然没有触发
updated()
. - 但是触发了一次
unbind()
,原因是:我们删除了一个元素.当元素从DOM中删除时就会触发指令的 unbind()函数
- 触发了
4
次componentUpdated()
,,之前认为的是display:block|none
,会触发,但这里也触发了4次,是不是由于剩下的4个item可能位置发生了改变也会触发这个???
测试绑定了位置改变的元素也会触发 componentUpdated()
<div id="card" v-gqs :class={move:isMove}></div>
<button @click="isMove = true">move</button>
componentUpdated2.gif
果然,不光是隐藏显示,就连位置发生了改变,也会触发 componentUpdated()
..
那既然是这样,只要元素的当前状态变了,就会触发 componentUpdated()
咯??
测试元素的内容发生了改变,将会触发componentUpdated()
componentUpdated3.gif既然元素只要能再肉眼范围内发生改变,就能触发 componentUpdated()
,
那可以使用这个特性来监控 input
元素的输入咯?(有没有卵用先放一边)
<input type="text" id='text' v-model="text" v-gqs>
componentUpdated(el) {
if (el.id === 'text') {
console.log(el.value)
}
}
componentUpdated4.gif
最后,没有测出来 updated()
的触发时机,倒是把 componentUpdated()
测试出来了. 😓.
componentUpdated()
触发就在于,当前绑定了这个指令的元素,有任意改变.
注意:这里的改变必须是指和vue数据建立联系.
- 显示/隐藏 (v-if & v-show)
- 样式改变 (:class={active:isActive})
- 内容改变 (v-model="text")
只要和vue数据绑定产生的不管是样式还是内容发生改变,那么就一定会触发 componentUpdated()
前提是:一定要和 vue
的数据建立联系!!!
前提是:一定要和 vue
的数据建立联系!!!
前提是:一定要和 vue
的数据建立联系!!!
所以 ,到底 update()
这个指令的钩子函数什么时候触发呢?
- 元素删除,新建不触发
- 各种形态转变也不触发....
所以,我就打算先把这个 updated()
钩子函数先放一放.
贴一个官方说明:
所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
为什么我如此执着的想测试出updated()事件触发机制呢?
官方说明:
指令的简写形式:
在很多时候,你可能想在 bind 和 update 时触发相同行为,而不关心其它的钩子。比如这样写:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
这里我怎么区分是触发的 bind()
还是 unpdate()
呢 ????
等等 是 update()
不是 updated()
!!!!!
我勒个去,钩子函数名字写错了!!!!!!!
我勒个去,钩子函数名字写错了!!!!!!!
我勒个去,钩子函数名字写错了!!!!!!!
难怪测试不出来.
既然,我之前的测试结果是:
-
bind()
--> 绑定触发,触发一次. -
inserted()
--> 元素插入父元素时触发. -
componentUpdated()
--> 元素状态发生改变时触发(包裹内容,样式) -
unbind()
--> 元素从DOM删除时触发.(仅仅是删除)
那按道理,既然是名字写错了update --> updated
...
那么 update
就是在 componentUpdated
之前触发的咯....
修改钩子函数名字再测试 update()
钩子函数.
既然 update()
& componentUpdated()
分成了两个钩子函数,执行顺序一前一后.
<input type="text" id='text' v-model="text" v-gqs>
// update !!! 不是 updated
update(el) {
// 所在组件的`VNode`更新时调用.
console.log(`update triggerd :${el.value}`)
},
componentUpdated(el) {
// 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
console.log(`componentUpdated triggerd:${el.value}`)
},
update->componentUpdated.gif
update()
和 componentUpdated()
前后执行顺序是对的.
简要的画一张图,来说明指令的这几个钩子函数
vue.指令声明钩子函数v2.0.png利用指令,可以做一些小事情.
既然,指令并不是一次性的活.
当绑定指令的元素的状态发生改变时(这里主要是指元素绑定的vue数据发生改变时),仍然会触发指令中的 update
函数.
那么我们可以利用指令的简写形式,来做一些有意思的事情.
核心思想就是:
当一个HTML元素设置了指令,那么在这个元素的状态发生改变时(由vue数据变化带来的带来的监控
),我们可以利用update()钩子函数监控到这个元素的变化,然后根据需要做一些其他的事情.
使用官方指定的指令简写模式:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
inserted
or update
.
当元素的状态发生改变时,就会触发 update
在写demo之前,还需要了解一下指令钩子函数的几个参数.
-
el
: 绑定指令的那个dom元素.(doucument.getElementById('el.id') -
binding
: 一个对象-
name
:v-gqs
==>gqs
不包括前面的v-
-
valule
: 指令后面的值v-gqs='abc'
value=abc -
oldValue
前一个值,只能在update
&componentUpdate
中使用. -
expression
:v-gqs='1+1'
,如果是value
=2
,如果是expression
=1 + 1
-
arg
: 指令传递的参数 ,比如v-gqs:hello
arg = hello -
modifiers
: 比如v-gqs.a.b
modifiers
= {a:true,b:true}
-
-
vnode
:Vue编译生成的虚拟节点. -
oldVnode
:上一个虚拟节点,仅在update
&componentUpdated
中可用.
Vue.directive('change-bgc', (el, binding,vnode,oldVnode) => {
el.style.backgroundColor = 'red'
console.log(`binding.name:${binding.name}`)
console.log(`binding.value:${binding.value}`)
console.log(`binding.expression:${binding.expression}`)
console.log(`binding.arg:${binding.arg}`)
console.log(`binding.modifiers:${JSON.stringify(binding.modifiers)}`)
console.log(`binging.oldValue:${binding.oldValue}`)
console.log(`vnode:${Object.keys(vnode).join(',')}`)
console.log(`oldVnode:${JSON.stringify(oldVnode)}`)
})
binding.name:change-bgc
binding.value:2
binding.expression:1+1
binding.arg:foo
binding.modifiers:{"left":true,"bottom":true}
binging.oldValue:undefined
vnode:tag,data,children,text,elm,ns,context,fnContext,fnOptions,fnScopeId,key,componentOptions,componentInstance,parent,raw,isStatic,isRootInsert,isComment,isCloned,isOnce,asyncFactory,asyncMeta,isAsyncPlaceholder
oldVnode:{"tag":"","data":{},"children":[],"raw":false,"isStatic":false,"isRootInsert":true,"isComment":false,"isCloned":false,"isOnce":false,"isAsyncPlaceholder":false}
DEMO
一个卡片应用,用户在页面上如果看到比较喜欢的卡片,可以钉在屏幕上.
先看看效果:
pinend2.gifHTML
<div id='app'>
<div v-pin="card1.pinded" class="card">
<a href="#" @click.prevent='card1.pinded = !card1.pinded'>钉住/取消</a>
<img src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1544621884322&di=ae2b10253c52ec699359f8c27a8097aa&imgtype=0&src=http%3A%2F%2Ff6.topitme.com%2F6%2F36%2Ff4%2F1118255893119f4366l.jpg"
alt="">
</div>
<div v-pin="card2.pinded" class="card">
<a href="#" @click.prevent='card2.pinded = !card2.pinded'>钉住/取消</a>
<img src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1544621884322&di=ae2b10253c52ec699359f8c27a8097aa&imgtype=0&src=http%3A%2F%2Ff6.topitme.com%2F6%2F36%2Ff4%2F1118255893119f4366l.jpg"
alt="">
</div>
</div>
.......
Vue.js
var app = new Vue({
el: '#app',
data: {
card1: {
pinded: false
},
card2: {
pinded: false
}
},
})
指令代码
// 如果你只需要执行绑定的 bind 和 update 两个事件,vue指令定义也配置了简写方式.
let toppx = 10
Vue.directive('pin', (el, binding) => {
if (binding.value) {
el.style.position = "fixed"
el.style.left = '300px'
let top = toppx
el.style.top = top + 'px'
toppx += 200
} else {
el.style.position = 'static'
toppx === 10 ? toppx : toppx-=200
}
})
核心代码解释一下:
<div v-pin="card1.pinded" class="card">
<a href="#" @click.prevent='card1.pinded = !card1.pinded'>钉住/取消</a>
</div>
- 我们定住的按钮是包含在使用了
v-pin
指令的一个DIV
内部的A
标签. -
A
标签本身是没有绑定任何指令的. - 但是在
A
标签的click
事件里,我们修改了card1.pinded
的值. -
DIV
里利用v-pin
指令绑定了card1.pinded
- 当
A
标签点击的时候,修改了card1.pinded
的值,这个值是vue.$data
的值,同时也修改了指令指向的那个值binding.value
. - 所以,最终就触发了
DIV
的v-pin
指令的update
方法.
组件指令
和过滤器一样,指令也分 全局指令 & 组件指令
组件指令语法:
var app = new Vue({
directives: {
innerDirective:{
bind() {
},
inserted() {
},
update() {
},
componentUpdated() {
},
unbind() {
}
}
})
简写模式:
// 主要是 bind & update 钩子函数
directives: {
simpleDirective:(el,binding) =>{
}
}