如何在Vue3中實現(xiàn)自定義指令(超詳細!)
在開發(fā)Vue項目時,大多數(shù)人都會使用到Vue內(nèi)置的一些指令,例如v-model、v-if等,在使用的時候不知道有沒有想過自己也來實現(xiàn)一個指令呢。本文就以Vue3項目為基礎(chǔ),從原理、方法到實際案例、注意事項,盡可能細致的講解如何自定義指令。
前言
我們需要明白為什么需要自定義一個指令,其實就是想更加簡潔地重復(fù)使用操作DOM的邏輯,這就和組件化和組合式函數(shù)差不多。
不管是Vue內(nèi)置的指令還是自定義的指令,都有類似于組件的生命周期,我們可以在不同的生命周期完成不同的邏輯操作,并綁定到組件元素上,這樣就產(chǎn)生了自定義指令。在Vue3中,我們有三種方式可以定義指令:
如果是在<script setup>定義組件內(nèi)的指令,有一個語法糖可以使用:任何以v開頭的駝峰式命名的變量都可以被用作一個自定義指令,然后在模板中使用。舉一個簡單的例子:在輸入框渲染后自動聚焦
<script setup> // 在模板中啟用 v-focus const vFocus = { mounted: (el) => el.focus() } </script> <template> <input v-focus /> </template>
運行效果:
如果是使用選項式,則自定義指令需要在
directives
選項中注冊。同上一個例子:<script> export default{ setup() {}, directives: { // 指令名 focus: { // 生命周期 mounted(el) { // 處理DOM的邏輯 el.focus(); }, } } } </script> <template> <input v-focus /> </template>
實現(xiàn)的效果也是和上一個例子一樣。
除了注冊組件內(nèi)指令,我們還可以自定義全局指令,這樣在所有的組件中都可以使用該指令
// main.js import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.directive('focus', { mounted(el) { el.focus(); } }) app.mount('#app')
實現(xiàn)效果也是一樣的。
這三種方式我們選擇最后一種,其他兩種方式可以按照類似的方式實現(xiàn)。
生命周期
指令的生命周期和組件的生命周期類似:
app.directive('focus', { created() { console.log('created'); }, beforeMount() { console.log('beforeMount'); }, mounted() { console.log('mounted'); }, beforeUpdate() { console.log('beforeUpdate'); }, updated() { console.log('updated'); }, beforeUnmount() { console.log('beforeUnmount'); }, unmounted() { console.log('unmounted'); } })
運行結(jié)果:
注意指令沒有beforeCreated
鉤子。
- created:在綁定元素的屬性前,或者事件監(jiān)聽器應(yīng)用前調(diào)用
- beforeMount:在元素被插入到DOM前調(diào)用,例如我們想要實現(xiàn)輸入框的自動聚焦,就不能在beforeMount鉤子中實現(xiàn)
- mounted:在綁定元素的父組件以及自己的所有子節(jié)點都掛載完畢后調(diào)用,這個時候DOM已經(jīng)渲染出來,我們實現(xiàn)輸入框自動聚焦也是在這個鉤子函數(shù)中實現(xiàn)
- beforeUpdate:綁定元素的父組件更新前調(diào)用
- updated:在綁定元素的父組件以及自己的所有子節(jié)點都更新完畢后調(diào)用
- beforeUnmount:綁定元素的父組件卸載前調(diào)用
- unmounted:綁定元素的父組件卸載后調(diào)用
每個鉤子函數(shù)都有對應(yīng)的參數(shù),接下來繼續(xù)看鉤子參數(shù)。
鉤子的參數(shù)
指令是為了能重用對DOM的操作邏輯,因此指令參數(shù)可以有1-4個參數(shù),其中必需的參數(shù)就是當前綁定的DOM元素。
語法:
created(el, binding, vnode, preVnode) {}
參數(shù)比較多,我們一個一個來學(xué)習(xí)。
el
:指令綁定到的DOM元素,可以用于直接操作當前元素,默認傳入鉤子的就是el參數(shù),例如我們開始實現(xiàn)的focus
指令,就是直接操作的元素DOMbinding
:這是一個對象,包含以下屬性:value
:在元素上使用指令時,傳遞給指令的值。例如:<div v-reverse="'hello'"></div>
,傳遞給reserve
指令的值就是hello
,我們可以拿到值并做相關(guān)處理oldValue
:之前的值,一般用于beforeUpate
和updated
鉤子函數(shù)中,例如:beforeUpdate(el, {oldValue: ''})
arg
:傳遞給指令的參數(shù),非必需,例如<div v-reverse:foo="'hello'"></div>
,那么傳遞給指令的參數(shù)就是foo
modifiers
:一個由修飾符構(gòu)成的對象,例如<div v-reverse.foo.bar="'hello'"></div>
,那么該修飾符對象為{foo: true, bar: true}
,我們經(jīng)常使用到的事件修飾符,其實和這個差不多。instance
:使用該指令的組件實例,注意不是DOMdir
:指令的定義對象
vnode
:綁定元素的地城VNodepreVnode
:之前的渲染中代表指令所綁定的元素的VNode,一般用于beforeUpate
和updated
鉤子函數(shù)中
可能看這些參數(shù)會一時迷糊,我們來看一個例子:
定義一個可翻轉(zhuǎn)輸入框輸入的指令,注意鉤子函數(shù)要選擇beforeUpdate
app.directive('reserve', { beforeUpdate(el, binding) { console.log(binding); el.innerText = binding.value ? binding.value.split('').reverse().join('') : ''; } })
在模板中使用:輸入框輸入值,div
會顯示反轉(zhuǎn)后的值
<script setup> import {ref} from 'vue' let hello = ref('') </script> <template> <input v-focus v-model="hello" /> <div v-reserve:foo.bar="hello"></div> </template>
運行結(jié)果:
結(jié)合該圖,是不是就更能理解鉤子參數(shù)的含義了。
簡化形式
我們在寫指令的時候,可以具體指定在哪些鉤子中執(zhí)行一些邏輯。有時候指令的鉤子不止一個,但是又是重復(fù)的邏輯操作時,重復(fù)寫一遍代碼顯然有點不夠優(yōu)雅。在Vue中,如果我們在自定義指令時,需要在mounted
和updated
中實現(xiàn)相同的行為,并且不關(guān)心其他鉤子的情況,那么我們開可以采用簡寫:
app.directive('color', (el, binding) => { // 這將會在mounted和updated時調(diào)用 el.style.color = binding.value; })
對象字面量
我們之前的例子中,傳遞給指令的值只有一個,如果我們想給指令傳入多個值應(yīng)該怎么操作呢?很簡單,傳入一個字面量對象即可,可以直接在模板中聲明,也可以使用響應(yīng)式對象,在使用時binding.value
就是一個對象了,而不是一個普通的值。
<script setup> import {ref, reactive} from 'vue' let hello = ref('') const obj = reactive({ hello: '', world: '' }) </script> <template> <input v-focus v-model="obj.hello" /> <div v-reserve:foo.bar="obj"></div> <!-- <div v-reserve:foo.bar="{hello: obj.hello, world: obj.world}"></div> --> </template>
對應(yīng)的,我們的指令也要小小的修改一下:
el.innerText = binding.value ? binding.value.hello.split('').reverse().join('') : '';
實現(xiàn)的效果還是和上面的保持一致。
在組件上使用指令
在元素上直接使用指令,我們可以在指令中操作DOM,這個已經(jīng)沒有問題了。那如果在組件上使用指令會怎樣呢?組件其實就是把一些DOM元素封裝起來,Vue3和Vue2不同,Vue3的模板中可以不止一個根節(jié)點。
我們新建一個Reverse.vue
,以便后續(xù)作為組件引入。
Vue2
:模板中只能有一個根節(jié)點,因此會報錯
// Reverse.vue <template> <div></div> <div></div> </template>
Vue3
:模板中可以不止一個根節(jié)點,正常
// Reverse.vue <template> <div></div> <div></div> </template>
既然指令是為了操作DOM元素,如果只有單個根節(jié)點那不會有問題,例如:
<script setup> ... import ReverseVue from './Reserve.vue' ... </script> <template> ... <ReverseVue v-reserve="obj"/> </template>
// Reverse.vue <template> <!-- v-reserve 指令會被應(yīng)用在此處 --> <div></div> </template>
如果模板中是多個根節(jié)點,就會拋出警告,并且不執(zhí)行指令
// Reverse.vue <template> <!-- v-reserve 不會作用,并且會拋出警告 --> <div></div> <div></div> </template>
結(jié)論:盡量不要在組件上使用自定義指令,除非能確定只會有一個根節(jié)點
幾個實用的自定義指令
以下舉例的指令都是全局指令
自動聚焦v-focus
聚焦比較特殊,兄弟元素間只會有一個聚焦,即將該指令作用于兩個兄弟輸入框上,只會自動聚焦一個
app.directive('focus', (el) => { el.focus(); })
防抖v-debounce
在實際項目開發(fā)中,經(jīng)常會聽到服務(wù)端的同事抱怨:前端怎么不做限流呀。前端做”限流“一般會采用防抖和節(jié)流,我們先來看如何實現(xiàn)防抖。
步驟:
- 首先我們得知道怎么寫一個防抖函數(shù)
- 然后需要將防抖函數(shù)與el節(jié)點綁定,為了通用的話,還需要考慮傳入事件類型
- 最后是卸載定時器等操作
app.directive('debounce', { mounted(el, binding) { // 至少需要回調(diào)函數(shù)以及監(jiān)聽事件類型 if (typeof binding.value.fn !== 'function' || !binding.value.event) return; let delay = 200; // 默認延遲時間 el.timer = null; el.handler = function() { if (el.timer) { clearTimeout(el.timer); el.timer = null; }; el.timer = setTimeout(() => { binding.value.fn.apply(this, arguments) el.timer = null; }, binding.value.delay || delay); } el.addEventListener(binding.value.event, el.handler) }, // 元素卸載前也記得清理定時器并且移除監(jiān)聽事件 beforeMount(el, binding) { if (el.timer) { clearTimeout(el.timer); el.timer = null; } el.removeEventListener(binding.value.event, el.handler) } })
在模板中使用:
<script setup> const handleClick = () => { console.log('防抖點擊'); } </script> <template> <button v-debounce="{fn: handleClick, event: 'click', delay: 200}">點擊試試</button> </template>
運行結(jié)果:
快速點擊按鈕并不會立即觸發(fā)handleClick
,而是會在指定的延遲時間后才會觸發(fā)。
節(jié)流v-throttle
節(jié)流和防抖類似,都是用于前端”限流“。不同的是,防抖是限制執(zhí)行次數(shù),多次密集的觸發(fā)只會執(zhí)行最后一次,無規(guī)律,更關(guān)注結(jié)果;節(jié)流是限制執(zhí)行頻率,有節(jié)奏的執(zhí)行,有規(guī)律, 更關(guān)注過程。
節(jié)流的實現(xiàn)和防抖差不多:
app.directive('throttle', { mounted(el, binding) { // 至少需要回調(diào)函數(shù)以及監(jiān)聽事件類型 if (typeof binding.value.fn !== 'function' || !binding.value.event) return; let delay = 200; el.timer = null; el.handler = function() { if (el.timer) return; el.timer = setTimeout(() => { binding.value.fn.apply(this, arguments) el.timer = null; }, binding.value.delay || delay); } el.addEventListener(binding.value.event, el.handler) }, // 元素卸載前也記得清理定時器并且移除監(jiān)聽事件 beforeMount(el, binding) { if (el.timer) { clearTimeout(el.timer); el.timer = null; } el.removeEventListener(binding.value.event, el.handler) } })
在模板中使用:
<script setup> import {reactive} from 'vue' const obj = reactive({ hello: '', world: '' }) const handleInput = () => { console.log('節(jié)流輸入框的值:', obj.hello); } </script> <template> <input v-throttle="{fn: handleInput, event: 'input', delay: 1000}" v-model="obj.hello" /> </template>
運行結(jié)果:
handleInput
并不會因為我在輸入框輸入時的快慢而觸發(fā),而是在固定的時間間隔內(nèi)觸發(fā)一次,這就是節(jié)流。
彈窗隱藏v-hide
在實際開發(fā)時會有這樣的需求:點擊某一個按鈕出現(xiàn)一個彈窗,然后點彈窗的其他區(qū)域時需要關(guān)閉彈窗,如果是點擊的彈窗本身,除非是關(guān)閉操作,否則不關(guān)閉彈窗。
想要實現(xiàn)這種效果,大多數(shù)人都會想到全局監(jiān)聽click
事件,并且判斷點擊的目標元素和我們的彈窗元素是不是同一個,如果不是那就隱藏彈窗。那么我們就來看看具體應(yīng)該怎么實現(xiàn):
app.directive('hide', { mounted(el, binding) { el.handler = function(e) { // 如果點擊范圍在綁定的元素范圍內(nèi),那么將不執(zhí)行指令操作,而是執(zhí)行原點擊事件 if (el.contains(e.target)) return; if (typeof binding.value.fn === 'function') { // 綁定給指令的如果是一個函數(shù),那么將回調(diào)并指定this binding.value.fn.apply(this, arguments) // 并不推薦使用style的方式來隱藏元素,這樣的話控制彈窗的變量就無法改變,所以推薦使用回調(diào)函數(shù) // el.style.display = 'none'; // 解除事件綁定 document.removeEventListener('click', el.handler) } } // 監(jiān)聽全局的點擊事件 document.addEventListener('click', el.handler) // 如果同步綁定全局事件不生效,可以采用異步的方式 // setTimeout(() => { // document.addEventListener('click', el.handler) // }, 0); }, // 解除事件綁定 beforeMount(el) { document.removeEventListener('click', el.handler) } })
在模板中使用:
<script setup> import {ref} from 'vue' let isShowModal = ref(false) const showModal = () => { isShowModal.value = true; } const cancleModal = () => { console.log('cancleModal'); isShowModal.value = false; } </script> <template> <button @click.stop="showModal">點擊顯示彈窗</button> <div class="modal" v-hide="{fn: cancleModal}" v-if="isShowModal"> <p>我是彈窗</p> <button @click.stop="cancleModal">關(guān)閉</button> </div> </template>
運行結(jié)果:
總結(jié)
本文盡量采用通俗易懂的方式,完整的梳理了如何在Vue3中自定義指令。合理的使用指令,可以更快的幫助我們解決問題,值得注意的是:
- 選擇指令的鉤子函數(shù)時需要明確,不同的鉤子函數(shù)呈現(xiàn)的效果是不一樣的
- 應(yīng)該及時卸載鉤子函數(shù)定義的全局變量、定時器、事件綁定等,避免影響其他組件使用,以及內(nèi)存泄漏
- 如果是涉及DOM操作的,我們第一時間應(yīng)該想到是不是可以抽離成指令的方式
到此這篇關(guān)于如何在Vue3中實現(xiàn)自定義指令的文章就介紹到這了,更多相關(guān)Vue3自定義指令內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue-cli Eslint在vscode里代碼自動格式化的方法
本篇文章主要介紹了Vue-cli Eslint在vscode里代碼自動格式化的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02vue前端項目打包成Docker鏡像并運行的實現(xiàn)
這篇文章主要介紹了vue前端項目打包成Docker鏡像并運行的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08vue解決Not?allowed?to?load?local?resource問題的全過程
這篇文章主要給大家介紹了關(guān)于vue解決Not?allowed?to?load?local?resource問題的相關(guān)資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2022-10-10詳解element上傳組件before-remove鉤子問題解決
這篇文章主要介紹了詳解element上傳組件before-remove鉤子問題解決,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04vue elementUI 表單校驗的實現(xiàn)代碼(多層嵌套)
這篇文章主要介紹了vue elementUI 表單校驗的實現(xiàn)代碼(多層嵌套),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11vue中使用element ui的彈窗與echarts之間的問題詳解
這篇文章主要介紹了vue中使用element ui的彈窗與echarts之間的問題詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10