vue2 vue3 中指令总结
vue 内置了一些指令,也提供了自定义指令的接口。
指令的作用:可把一些可复用的逻辑封装成指令,以实现逻辑复用,尤其是需要直接操作 DOM 时,可把这些操作封装成指令,能极大提高代码复用性和可维护性。
指令按照使用范围看,分为全局指令 和局部指令(在某个组件内部使用的)。
指令和组件一样,具有一些在特定时期执行的函数,叫作指令钩子,就是通过它们定义指令的。
使用方式
有一个 v-test
指令。
html
<template>
<div v-test:disabled.foo="'directive'">
<h1>{{ msg }}</h1>
</div>
</template>
:
之后的是指令参数,类似 v-on:keyup
中的 keyup
。
.
之后的时指令修饰符,foo 是修饰符。
参数只能有一个,修饰符可有多个。
v-test:disabled:boo.foo.zoo="msg"
在指令内部,可获取到这样的对象:
js
{
arg: "disabled:boo"
modifiers: {
foo: true,
zoo: true
}
}
希望不同情况下绑定不同的参数,可使用动态参数。
v-test:[argu].foo.zoo="msg"
参数必须在修饰符之前。
指令的等号后面是指令表达式,其值对应 binding
对象的 value 属性。
binding
一个对象:
js
{
arg: "disabled",
expression: "msg",
modifiers: {
foo: true
},
name: "test",
value: "你好",
}
vue2 指令定义方式
以插件的形式定义一个全局 v-click-outside
:
js
export const clickOutside = (Vue, options) => {
Vue.directive('clickOutside', {
inserted(el, binding, vnode) {
const {
value
} = binding
// const _this = vnode.context
// NOTE 技巧:处理函数挂载在元素上,方便解绑时移出事件监听
el.onClick = ({
target
}) => {
if (el.contains(target)) {
// 点击内部
console.log('clickInside')
} else {
// 点击外部
console.log('clickOutside')
value && value()
}
}
document.addEventListener('click', el.onClick, false)
},
unbind(el, binding, vnode) {
document.removeEventListener('click', el.onClick, false)
},
})
}
注册插件:
main.js
js
Vue.use(clickOutside)
使用:
html
<template>
<div>
<h3>测试 v-click-outside</h3>
<div v-click-outside="onClickOutside">
<p>测试点击外部</p>
<p>测试点击外部</p>
<p>测试点击外部</p>
<p>测试点击外部</p>
</div>
</div>
</template>
<script>
export default {
name: 'ClickOutsideDemo',
data() {
return {
msg: 'Hello web components in stencil!',
}
},
methods: {
onClickOutside() {
console.log('onClickOutside')
console.log(this.msg)
},
},
}
</script>
点击到div
外部时,就会执行onClickOutside
。
js
Vue.directive('directive-name', options)
options 是一个对象,包含一些生命周期钩子,这些生命周期都是可选的。
按照执行顺序可:
bash
bind # 只调用一次,指令绑定到元素时调用,父元素可能不存在。做初始化工作
⬇️
inserted # 只调用一次,被绑定的元素插入到父节点,父节点存在,此时可能被绑定元素还每插入文档中。
⬇️
update # 此时组件还没更新完毕,拿不到最新的数据
⬇️
componentUpdated # 此时组件已经更新完毕 能拿到最新
⬇️
unbind # 只调用一次,指令和元素解绑,可做一些收尾工作
简写方式:
js
Vue.directive('color-swatch', function(el, binding) {
el.style.backgroundColor = binding.value
})
在 bind 和 update 时触发相同行为,而不关心其它的钩子,此时推荐使用函数方式定义指令。
钩子的参数
js
bind(el, binding, vnode)
inserted(el, binding, vnode)
update(el, binding, vnode, oldVnode) // update componentUpdated 还有额外的 oldVnode
componentUpdated(el, binding, vnode, oldVnode)
unbind(el, binding, vnode)
指令钩子函数的参数,主要关注
el
和binding
。
el
是绑定指令的元素,可对其进行 DOM 操作。
binding
一个对象:
js
{
arg: "disabled",
expression: "msg",
modifiers: {
foo: true
},
name: "test",
value: "你好",
}
update
和 componentUpdated
钩子函数,binding 对象还有 oldArg
和 oldValue
属性。
指令钩子和组件生命周期的执行顺序是怎样的?
挂载组件
bash{7,
beforeCreate
⬇️
created
⬇️
beforeMount
⬇️
bind # 指令 绑定到元素时调用,父元素可能不存在
⬇️
inserted # 指令 被绑定元素插入父元素时调用,父元素一定存在
⬇️
mounted
更新组件
bash{3,5}
beforeUpdate
⬇️
update # 指令,此时组件还没更新完毕,拿不到最新的数据
⬇️
componentUpdated # 指令 此时组件已经更新完毕 能拿到最新的数据
⬇️
updated
销毁组件
bash
beforeDestroy
⬇️
unbind # 指令 组件在销毁之前调用,仍然能拿到组件的数据
⬇️
destroyed
重建组件时
bash
beforeCreate
⬇️
created (重建)
⬇️
beforeMount
⬇️
bind # 注意这里,指令绑定这个钩子函数,将会拿不到重建后的最新数据
⬇️
beforeDestroy (组件销毁)
⬇️
unbind # 指令
⬇️
destroyed
⬇️
inserted # 指令 使用该钩子函数,能拿到重建后的数据
⬇️
mounted
结论:只有
inserted
和componentUpdated
生命周期钩子,在执行时组件的 DOM 已经更新完毕,可放心使用。它们可获取到组件更新后的数据,指令绑定的元素的父元素也已经存在。
如何在指令生命周期中使用this
或者访问组件实例?
直接使用 this
为 undefined,可使用 vnode.context
获取:
js
inserted(el, binding, vnode) {
// setTile 是组件 methods 里的方法
vnode.context.setTile(el)
},
componentUpdated(el, binding, vnode, oldVnode) {
vnode.context.setTile(el)
},
vue3 中的指令
vue3 的指令钩子、钩子的参数和定义方式有变化。
定义方式
全局指令
js
app.directive('directive-name', directiveOptions)
简写形式:
js
app.directive('directive-name', (el, binding) => {}) // mounted 和 updated 操作相同
局部指令
script setup 使用 v
开头命名。
html
<script setup>
// 注册一个局部的自定义指令,需要以小写v开头
const vFocus = {
mounted(el, binding, vNode) {
console.log(el)
console.log(binding)
console.log(vNode)
// 获取input,并调用其focus()方法
el.focus()
},
}
</script>
<template>
<input v-focus />
</template>
setup 函数方式:
html
<script>
export default {
directives: {
focus: {
mounted(el, binding, vNode) {
// 获取input,并调用其focus()方法
el.focus()
},
},
},
setup() {},
}
</script>
指令钩子的变化:
js
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {},
// 在元素被插入到 DOM 前调用, 可执行一些初始化工作
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用,收尾工作
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {},
}
常用的钩子:
bash
beforeMount # 类似 vue2 bind
mounted # 类似 vue2 inserted
updated # 类似 vue2 componentUpdated
beforeUnmount # 类似 vue2 unbind
binding 的参数有变化:增加了 instance
属性用于获取组件实例。
创建一个检测点击 div 外部的指令:
ts
import type { App } from 'vue'
export const clickOutside = (app: App, options: any) => {
app.directive('clickOutside', {
mounted(el, binding) {
const { value, instance } = binding
// NOTE 技巧:处理函数挂载在元素上,方便解绑时移出事件监听
el.onClick = (event: Event) => {
if (el.contains(event.target)) {
console.log('clickInside')
} else {
// 点击外部
console.log('clickOutside')
// console.log(instance)// 组件实例
value && value()
}
}
document.addEventListener('click', el.onClick, false)
},
beforeUnmount(el) {
console.log('beforeUnmount')
document.removeEventListener('click', el.onClick, false)
},
})
return app
}
相同的功能使用 hook 实现:
js
import {
onMounted,
onBeforeUnmount,
ref
} from 'vue'
export function useOnClickOutside(DOM = null, callback) {
const isClickOutside = ref(false)
function handleClick(event) {
if (DOM.value && !DOM.value.contains(event.target)) {
callback()
isClickOutside.value = true
}
}
onMounted(() => {
document.addEventListener('mousedown', handleClick)
})
onBeforeUnmount(() => {
document.removeEventListener('mousedown', handleClick)
})
return isClickOutside
}
hook vs 指令
使用方式:hook 在 script 里,指令绑定到模板
定义方式:hook 更加简单,只需记住组件的生命钩子,复用更加方便,指令略微繁琐,需要额外记忆指令钩子。
hook 能返回响应是对象,指令做不到。
hook 能组合使用,指令不行。多个指令用在同一个元素上,也算一种组合?
hook 优势更大,更加灵活
指令钩子函数和组件生命周期的执行顺序
挂载阶段:
bash
setup
onBeforeMount
created # 指令钩子
beforeMount # 指令钩子
mounted # 指令钩子
onMounted
销毁阶段:
bash
onBeforeUnmount
beforeUnmount # 指令钩子
unmounted # 指令钩子
onUnmounted
比较好实践:只定义 mounted、updated 和 beforeUnmount 钩子。
指令和 render 函数
指令用在 jsx 上
和用到 html 上一样。
jsx
function JSXDemo() {
return (
<>
<h3>v-copy 用jsx上</h3>
<div v-copy={'这是复制的内容'} style={{ color: 'red' }}>
点击复制
</div>
<h3>v-auth</h3>
<button v-auth:disabled={'li'} class="button">
无权限,被禁用
</button>
<br />
<button v-auth="key3">无权限删除</button>
<br />
<button v-auth="key">有权限</button>
<br />
<ElButton type="primary" v-auth="key">
按钮
</ElButton>
</>
)
}
指令和 render 函数一起使用
使用 widthDirectives
函数注册指令。
withDirectives(VNode, [[vDirective,value, arg, modifiers]])
js
function RenderDemo() {
// withDirectives(VNode, [[vDirective,value, arg, modifiers]])
const Input = h('input', {
type: 'text',
value: 'hello'
})
const InputWithFocus = withDirectives(Input, [
[directiveObj.focus, true]
])
const DivWithCopy = withDirectives(h('div', '这是复制的内容'), [
[directiveObj.copy, '这是复制的内容'],
])
const BtnWithAuth = withDirectives(h('button', '无权限,被禁用'), [
[directiveObj.auth, 'li', 'disabled'],
])
return h('div', [
h('h3', 'v-copy 用h上'),
DivWithCopy,
['点击复制', h('br'), InputWithFocus],
h('br'),
h('h3', 'v-auth'),
BtnWithAuth,
])
}
不能使用全局指令
最佳实践
- 传递多个值,使用对象
html
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
js
app.directive("demo", (el, binding) => {
console.log(binding.value.color); // => "white"
console.log(binding.value.text); // => "hello!"
});
- 不在组件上使用自定义指令
原因:当组件有多个根元素时,它不知道绑定到那个元素上。
其他问题
如何使用代码测试指令呢?
我搜索不到相关教程。
有请大佬解答。
参考
Use Vue3 Custom Directives Like a Pro
小结
- 指令是复用逻辑的有效方式,尤其是遇到需要直接操作 DOM 的情况。
- vue2 中常用的指令钩子:inserted、componentUpdated 和 unbind。
- vue3 中常用的指令钩子:mounted、updated 和 beforeUnmount。
- vue3 中
script setup
定义局部指令使用v
开头。 - vue3 的 binding 对象添加了
instance
属性。 - 相同的功能 hook 和指令都能实现,hook 优势更大。