Vue-自定义指令
上面说完了 Vue 的内置指令,了解了这些内置指令的使用及原理,接下来我们来说说 Vue 的自定义指令。自定义指令定义v-show :Vue 的内置指令,通过这个指令能展示和隐藏节点 ,其实也就是Vue底层控制了该节点的 display 属性。自定义指令:在构建项目过程中,虽然我们都是使用的组件形式,但是在某些情况下,我们仍然需要对普通DOM元素进行底层操作,这个时候就会用到自定义指令。自定义指令
上面说完了 Vue 的内置指令,了解了这些内置指令的使用及原理,接下来我们来说说 Vue 的自定义指令。
自定义指令定义
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 需要怎么来理解呢?上面说过了,自定义指令的执行时机是 指令与元素成功绑定、当所在指令的组件或者叫模板被重新解析时,所以可以这么来理解
- 初始化之后,指令与元素绑定,此时只是在内存中建立了这个关系,元素并没有渲染到页面(虽然我们写的代码上 确实存在这个 input 节点,但是这只是一个模板啊,还需要经过 Vue 的模板编译过程才会展示到页面上)
- 在我点击之后,此时模板被重新解析,重新调用自定义指令,但是此时页面上是存在 这个 input 框的,所以 el.focus() 这个方法找到了 DOM 元素,进而实现了自动聚焦 的效果
解决办法
其实解决办法就是,在 el 节点已经存在在页面上之后,再来执行 el.focus() ,但是我们要怎么获取这个时间点呢?我们到现在为止使用的还是 函数式的自定义指令 ,但是函数式自定义指令是无法获取DOM已经渲染到页面上这个准确时间点的,这也就是开篇说的,函数式无法处理一些细节问题。既然如此,那我们来试试对象形式的自定义指令
focus: {
bind() { },
inserted() { },
update() { },
}
对向形式的自定义指令储存在三个钩子函数,均为可选项
-
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 -
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 -
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
}
}
)
踩坑记录
1、自定义指令驼峰命名
将上面的 v-focus 自定义指令改个名,写成驼峰写法,v-bigFocus,咋一看这也没问题啊,平常不也都这么写么,但是这就出错啦,Vue 不支持 驼峰命名的自定义指令
<input type="text" v-bigFocus:value='count'>
bigFocus(el, binding) {
el.value = binding.value
},
Vue 规定,如果自定义指令是驼峰的话,需要改成使用 - 连接两个单词,例如 v-big-focus
"big-focus": function (el, binding) {
el.value = binding.value
},
此时页面展示正确,且 input 框中的值也会跟随改变
上面的函数还可以简写,因为对象内部的属性 key 本来就是字符串形式的,只不过我们一般都是简写方式,直接不写成字符串
"big-focus"(el, binding) {
el.value = binding.value
},
2、自定义指令内部的this指向
我们之前说的,凡是由 Vue 管理的函数,内部this 都是指向 vm 实例的,但是在 自定义指令内部却不是这样的。
可以看到,自定义指令内部的三个钩子函数的this指向,都是指向的 window ,这是因为 只要是出现在 自定义指令中,那就是代表我需要对 DOM 进行操作,那我把 真实DOM节点( el ) 以及节点中对应绑定的属性值( binding )都给你那不就得了么,那你还要 vm 实例对象干啥呢?,所以 Vue 在这里并不会去 维护这个 this。然后就是 函数式和对象式的自定义指令中的this,都是指向 window
本章总结
1、定义指令的类型以及语法:
局部指令:
函数式: new Vue({ directives :{ '指令名称' :配置对象}})
对象式:new Vue({ directives :{ '指令名称' :回调函数}})
全局指令:
函数式:Vue.directive(指令名称,回调函数)
对象式:Vue.directive(指令名称,配置对象)
2、配置对象常用的三个钩子函数
- bind:指令与元素成功绑定时调用(此时只是将元素与指令绑定,但是真实DOM节点未渲染)
- inserted:指令所在的元素被插入父节点时调用(真实DOM已渲染到页面上,能获取真实DOM)
- update:指令所在模板更新时调用( 不单纯是指令绑定数据更新,而是只要data内部数据变化)
3、指令在使用时需要加上 v- ,但是在定义时不需要
4、指令名称如果是多个单词,需要使用 kebab-case( 连字符 - ),不能直接使用驼峰命名
更多推荐
所有评论(0)