自定义指令定义
v-show :Vue 的内置指令,通过这个指令能展示和隐藏节点 (Vue底层控制了该节点的 display 属性)
自定义指令:在构建项目过程中,虽然我们都是使用的组件形式,但是在某些情况下,我们仍然需要对普通DOM元素进行底层操作,这个时候就会用到自定义指令。
自定义指令既可以像 v-show 一样,不搭配属性值,也可以像 v-text配置属性值来实现特殊效果
自定义指令分类
自定义指令分为组件内部的自定义指令 和 全局自定义指令
组件内部的自定义指令:实现将现有 数值放大 10 倍
<div id='root'>
<div>当前count值是:{{count}}</div>
<div>放大十倍后的count值是 <span v-big='count'></span></div>
<div><button @click='count++'>点击+1</button></div>
</div>
const vm = new Vue({
el: '#root',
data() {
return {
count: 1
}
},
})
页面报错,提示 big 指令解析失败。但是,这里的指令报错是 big,而不是写在节点上的 v-big 指令,这说明了 Vue 自动将我们的 自定义指令添加上了 v- 前缀。
我们在组件内部注册一个自定义指令,这样页面上就不会报错了,但是放大后的值还是无法展示
directives: {
// 函数式自定义指令
big() { },
// 对象式自定义指令
big:{
}
}
组件内部自定义指令( directives )和 data 平级,类似于 过滤器 ,也是一个属性,内部可以注册多个自定义指令,内部的注册的自定义指令有两种写法,分别是函数式和对象式,函数式精简一点,对象式复杂一点,但是能处理一些细节上的问题。
自定义指令的使用
我们先注册函数式自定义指令来解决这个简单的需求
1、 v-text 的作用是拿到当前 count 且展示到 当前 span 标签内部,但是同样的方式对于 v-big 是不生效的,那 自定义 v-big 又是怎么工作的呢?说到这里可能会想到 计算属性的使用方式,通过返回一个值,然后替换掉当前的插值语法,来实现 处理后数据的展示,那我们也先这样来试试。
directives: {
big() {
return 900
}
}
通过页面展示发现,即使 给了返回值,还是没有展示出来,说明 v-big 并不是这样使用的。
通过查看 文档 之后发现,该自定义的指令钩子函数接收四个参数,分别是
el:指令所绑定的元素,是真实 DOM节点,可以通过该元素来操作 DOM
binding:一个对象,里面包括很多属性,但是基本上只关注 value 属性,因为这是 指令绑定的值
vnode:Vue 编译生成的虚拟节点
oldVnode:Vue 编译生成的上一个虚拟节点(仅在 update 和 componentUpdated 使用)
现在,我们能够得到这个 绑定的真实 DOM 节点,还能拿到当前节点绑定的 Value 值,那么我们就能以此来实现,针对改DOM元素的操作,例如完成上面的需求
directives: {
big(el, binding, vnode, oldVnode) {
console.log(el, binding, vnode, oldVnode)
el.innerText = binding.value * 10
}
}
可以看到,通过直接操作 DOM 节点的 innertext ,放大10倍之后的值已经展示在页面上了。所以注册的自定义指令是并不需要返回值的,,而是通过直接操作 DOM 元素,来更改页面展示
调用自定义指令的时机
在上面的成功案例中,除了在第一次初始化时,自定义指令会调用一次,当点击按钮使得 count 增加时,发现自定义指令也是在被处重复调用的
这是不是就说明了,一旦自定义指令绑定的数据发生了变化,那么自定义指令就会被调用呢?就和计算属性一样,一旦以来的值发生了改变,就会重新调用计算属性方法。那我们验证一下,添加一个新的属性 name,值为 al,然后我们在控制台上修改 name 属性
.....
<div>{{name}}</div>
data() {
return {
name:'al',
.....
}
},
我们在控制台上修改了 name 属性,此时,与自定义指令 v-big 关联的 count 属性,并没有被修改,但是 自定义指令还是被触发了,这是因为 Vue 默认,一旦 data 中的响应式数据发生改变之后,就会重新解析模板内容,进而重新调用 自定义指令获取最新的值
所以
1、在初始化时,指令与元素成功绑定时( 是绑定,并不是渲染到页面),自定义指令会第一次调用
2、当所在指令的组件或者叫模板被重新解析时( 包括但不限于 data 内部数据更改 )
自定义指令对象形式
需求升级,现在有一个input 框,绑定的还是 count 值,但是我需要在页面初始化的时候,input 框自动聚焦。可能有人会想到使用 autofocus 这个属性。
<input type="text" autofocus v-bind:value='count'>
data() {
return {
count: 1
}
},
但是问题是这个属性也不是所有浏览器都兼容的
所以为了实现这个效果,还是需要我们自己用js 来处理
1、自定义指令 focus,目的在于给 input 绑定 count 值,且使得 input 初始渲染之后直接聚焦
<input type="text" v-focus:value='count'>
directives: {
focus(el, binding) {
el.value = binding.value
el.focus()
}
}
理论上来说,这样写是没有问题的,直接操作 DOM ,给 DOM 绑定 value 值,且利用本身的 focus 方法,使得 input 框自动聚焦。展示效果如下:
但是实际上有问题,value 值是绑定了,但是自动聚焦却是没有做到,这又是为啥呢?
还有个问题,我添加一个按钮,点击之后 count 自增1,自定义指令代码不变
<input type="text" v-focus:value='count'>
<div><button @click='count++'>点击+1</button></div>
这个时候突然发现,这个input 框在我点击按钮 改变 count 之后,它自动聚焦了。这又是为什么呢?
其实应该这么说,这两个问题其实综合起来是一个问题,el.focus 这段代码其实是执行了的,但是执行的时机不对。我们用原生的js也可以实现这个效果
<button onclick="creatInput()">点我创建一个 input 元素</button>
creatInput = () => {
let i = document.createElement('input')
document.body.appendChild(i)
i.focus()
}
但是如果调换了 i.focus() 的位置,那就不会自动聚焦了
creatInput = () => {
let i = document.createElement('input')
i.focus()
document.body.appendChild(i)
}
这是因为像这样的操作,需要在 input 这个元素添加到页面中之后,才会触发,否则页面都没有这个元素,设置 focus 没有意义。
所以,在自定义指令中,el.focus() 这句代码其实是执行了的,但是因为执行时机不对,所以页面初始化之后,不会自动聚焦。
那针对 Vue 需要怎么来理解呢?上面说过了,自定义指令的执行时机是 指令与元素成功绑定、当所在指令的组件或者叫模板被重新解析时,所以可以这么来理解
1、初始化之后,指令与元素绑定,此时只是在内存中建立了这个关系,元素并没有渲染到页面(虽然代码上 确实存在这个 input 节点,但是这只是一个模板,还需要经过 Vue 的模板编译过程才会展示到页面上)
2、在点击之后,此时模板被重新解析,重新调用自定义指令,但是此时页面上是存在 这个 input 框的,所以 el.focus() 这个方法找到了 DOM 元素,进而实现了自动聚焦 的效果
解决办法
其实解决办法就是,在 el 节点已经存在在页面上之后,再来执行 el.focus() ,但是要怎么获取这个时间点呢?我们到现在为止使用的还是 函数式的自定义指令 ,但是函数式自定义指令是无法获取DOM已经渲染到页面上这个准确时间点的,这也就是开篇说的,函数式无法处理一些细节问题。既然如此,那我们来试试对象形式的自定义指令
focus: {
bind() { },
inserted() { },
update() { },
}
对向形式的自定义指令储存在三个钩子函数,均为可选项
1、bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2、inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3、update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
这三个钩子函数都能像 函数式自定义指令一样,接收到 el 和 binding ,VNode 和 oldVonde四个参数,但是后面两个参数使用频率较少,所以不详细介绍了。完整形式则是这样
focus: {
bind(el, binding) {
el.value = binding.value
},
inserted(el, binding) {
el.focus()
},
update(el, binding) {
console.log('update')
},
}
前面两个钩子函数这样写是没问题的,页面初始化之后展示的也是对的。
但是当点击按钮,更改 count 之后,模板重新编译,此时会跳过 bind 钩子 和 inserted 钩子,因为页面上已经存在这个节点了,,此时会直接触发 update 钩子,但是 update 钩子函数中没有做任何操作,所以 input 中展示的还是初始化之后的 count
要想更改 count 之后,input 框中的值也跟着变化,我们还需要在 update 钩子函数中做操作,重新将 input 中绑定的值替换为最新的 count 值。
update(el, binding) {
el.value = binding.value
},
这样看起来, bind 钩子 和 update 钩子 里面的代码是相同的,做的事情也是相同的,其实这就是 如果只看这两个钩子的话,这就是函数式自定义指令,只考虑初始化和更新后的操作,而不关心节点是否挂载
全局自定义指令:接收两个参数,第一个是字符串形式的指令名称,第二个是指令处理方式( 可以是函数式,也可以是对象式,具体就是把局部的处理函数或处理对象完整粘贴过来就行)
之前就有说过,自定义指令和自定义过滤器其实是极其类似的,组件内的 自定义过滤器只能在组件内部使用,同样的,组件内的自定义过滤器也只能在组件内部使用。那么全局的过滤器和全局的自定义指令同样也都是挂载到 全局 Vue 实例上的,且全局的过滤器和自定义指令都是一次只能注册一个,所以方法都是不带s复数形式的( filter ,directive )。例如
函数式的全局自定义指令:
Vue.directive('big', (el, binding) => {
el.value = binding.value
})
对象式的全局自定义指令:
Vue.directive('big', {
bind(el, binding) {
el.value = binding.value
},
inserted(el, binding) {
el.focus()
},
update(el, binding) {
el.value = binding.value
}
}
)