您好,登錄后才能下訂單哦!
今天小編給大家分享一下Vue中的自定義指令怎么實現的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
首先梳理思路:原生input
控件與組件的實現方式需要區分,input
的實現較為簡單,我們先實現一下input
的處理。
首先我們先定義一個不做任何操作的指令
上面的注釋中詳細的說明了各個鉤子函數的調用時機,因為我們是給組件上添加 簡單說一下Vue.directive('mymodel', {
//只調用一次,指令第一次綁定到元素時調用。在這里可以進行一次性的初始化設置。
bind(el, binding, vnode, oldVnode) {
},
//被綁定元素插入父節點時調用 (僅保證父節點存在,但不一定已被插入文檔中),需要父節點dom時使用這個鉤子
inserted(el, binding, vnode, oldVnode) {
},
//所在組件的 VNode 更新時調用,**但是可能發生在其子 VNode 更新之前**。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前后的值來忽略不必要的模板更新 (詳細的鉤子函數參數見下)。
update(el, binding, vnode, oldVnode) {
},
//指令所在組件的 VNode **及其子 VNode** 全部更新后調用。
componentUpdated(el, binding, vnode, oldVnode) {
},
只調用一次,指令與元素解綁時調用。
unbind(el, binding, vnode, oldVnode) {
},
})
input
事件和value
綁定,因此我們在bind
這個鉤子函數中定義即可。所以我們把其他的先去掉,代碼變成這樣。Vue.directive('mymodel', {
//只調用一次,指令第一次綁定到元素時調用。在這里可以進行一次性的初始化設置。
bind(el, binding, vnode, oldVnode) {
}
})
bind
函數的幾個回調參數,el
是指令綁定組件對應的dom
,binding
是我們的指令本身,包含name
、value
、expression
、arg
等,vnode
就是當前綁定組件對應的vnode
結點,oldVnode
就是vnode
更新前的狀態。
接下來我們要做兩件事:
綁定input
事件,同步input
的value
值到外部
value
值綁定,監聽value
的變化,更新到input
的value
這對于input
原生組件比較容易實現:
這里解釋一下上面的代碼,//第一步,添加inout事件監聽
el.addEventListener('input', (e) => {
//context是input所在的父組件,這一步是同步數據
vnode.context[binding.expression] = e.target.value;
})
//監聽綁定的變量
vnode.context.$watch(binding.expression, (v) => {
el.value = v;
})
vnode.context
是什么呢,他就是我們指令所在組件的上下文環境,可以理解就是指令綁定的值所在的組件實例。不熟悉vnode
結構的同學建議先看一下官方的文檔,不過文檔描述的比較簡單,不是很全面,所以最好在控制臺log
一下vnode
的對象看一下它具體的結構,這很有助于我們封裝自定義指令,對理解Vue
原理也很有幫助。
我們可以通過context[binding.expression]
獲取v-model上到綁定的值,同樣可以修改它。上面的代碼中我們首先通過在添加的input事件中操作vnode.context[binding.expression] = e.target.value
同步input
的value
值到外部(context
),與使用@input
添加事件監聽效果是一樣的;然后我們需要做第二件事,做value
值的綁定,監聽value
的變化,同步值的變更到input
的value
上,我們想到我們可以使用Vue實例上的額$watch
方法監聽值的變化,而context
就是那個Vue
實例,binding.expression
就是我們想要監聽的屬性,如果我們這樣寫
那么 至此, 接下來我們要處理的是自定義組件的邏輯, 聲明一下,上面的實現不是<input v-mymodel='message'/>
binding.expression
就是字符串'message'
。所以我們想下面的代碼這樣監聽綁定的響應式數據。//監聽綁定的變量
vnode.context.$watch(binding.expression, (v) => {
el.value = v;
})
input
的v-mymodel
的處理就完成了(當然input
組件還有type
為checkbox
,radio
,select
等類型都需要去特別處理,這里就不再一一處理了,感興趣的同學可以自己嘗試去完善一下),但是對于非原生控件的組件,我們要特殊處理。
因此我們完善代碼如下:Vue.directive('mymodel', {
//只調用一次,指令第一次綁定到元素時調用。在這里可以進行一次性的初始化設置。
bind(el, binding, vnode, oldVnode) {
//原生input組件的處理
if(vnode.tag==='input'){
//第一步,添加inout事件監聽
el.addEventListener('input', (e) => {
//context是input所在的父組件,這一步是同步數據
vnode.context[binding.expression] = e.target.value;
})
//監聽綁定的變量
vnode.context.$watch(binding.expression, (v) => {
el.value = v;
})
}else{//組件
}
}
})
//vnode的結構可以參見文檔。不過我覺得最直觀的方法就是直接在控制臺打印處理
let {
componentInstance,
componentOptions,
context
} = vnode;
const {
_props
} = componentInstance;
//處理model選項
if (!componentOptions.Ctor.extendOptions.model) {
componentOptions.Ctor.extendOptions.model = {
value: 'value',
event: 'input'
}
}
let modelValue = componentOptions.Ctor.extendOptions.model.value;
let modelEvent = componentOptions.Ctor.extendOptions.model.event;
//屬性綁定,這里直接修改了屬性,沒有想到更好的辦法,友好的意見希望可以提出
_props[modelValue] = binding.value;
context.$watch(binding.expression, (v) => {
_props[modelValue] = v;
})
//添加事件處理函數,做數據同步
componentInstance.$on(modelEvent, (v) => {
context[binding.expression] = v;
})
vue
源碼的實現方式,vue
源碼中實現v-model
更加復雜一點,是結合自定義指令、模板編譯等去實現的,因為我們是應用級別的封裝,所以采用了上述的方式實現。
實現此v-mymodel
需要同學去多了解一下Vnode
和Component
的API
,就像之前說的,最簡單的方法就是直接在控制臺中直接打印出vnode
對象,組件的vnode
上有Component
的實例componentInstance
。
接下來簡單說一下上面的代碼,首先我們可以在componentOptions.Ctor.extendOptions
上找到model
的定義,如果沒有的話需要設置默認值value
和input
,然后分別對想原生input
的處理一樣,分別監聽binding.expression
的變化和modelEvent
事件即可。
需要注意的是,我們上面的代碼直接給_prop
做了賦值操作,這實際上是不符合規范的,但是我目前沒有找到更好的方法去實現。
下面?是完整的源碼:
上文我們通過封裝v-mymodel
為各位同學展示了如何封裝和使用自定義指令,接下來我把自己在生產實踐中使用自定義指令的一些經驗分享給大家,通過實例,我相信各位同學能夠更深刻的理解如何在在應用中封裝自己的指令,提高效率。
下面我們定義一個v-permission
指令用于全平臺的權限控制
role:角色控制;
currentUser:當前登錄人判斷;當前用戶是否是業務數據中的創建人或者負責人
bussinessStatus:業務狀態判斷;
every:與操作;
some:或操作;
示例代碼
//定義權限類型
const permissionType = {
ROLE: 'role',
CURRENTUSER:'currentUser',
BUSSINESSSTATUS: 'bussinessStatus',
MIX_EVERY: 'every',
MIX_SOME: 'some'
}
export default {
//只調用一次,指令第一次綁定到元素時調用
bind: function () {
},
//當前vdom插入到真實dom時,因為是對dom的樣式操作,在這里操作
inserted: function (el, binding) {
let show = false;
show=processingType(binding.arg,binding.value);
el.style.display = `${show ? 'inline-block' : 'none'}`
},
//所在組件的VNode更新時調用,狀態更新后需要更新顯示狀態
update: function (el, binding) {
//避免無效的模板更新
if(binding.value===binding.oldValue) return;
let show = false;
show=processingType(binding.arg,binding.value);
el.style.display = `${show ? 'inline-block' : 'none'}`
},
//指令所在組件的 VNode 及其子 VNode 全部更新后
componentUpdated: function (el, binding) {
},
unbind: function () {
},
}
//處理不同類型的權限控制
function processingType(type,value){
let values=[];
switch (type) {
case permissionType.ROLE:
return permissionByRole(value);
case permissionType.CURRENTUSER:
return permissionCreater(value);
case permissionType.BUSSINESSSTATUS:
return permissionBusinessStatus(value);
case permissionType.MIX_EVERY:
for(let type in value){
values.push(processingType(type,value[type]))
}
return values.every(v=>{
return v;
})
case permissionType.MIX_SOME:
for(let type in value){
values.push(processingType(type,value[type]))
}
return values.some(v=>{
return v;
})
default:
return false;
}
}
//業務狀態判斷
function permissionBusinessStatus(bindingValue){
return bindingValue.status==bindingValue.value;
}
//當前用戶?
function permissionCreater(bindingValue){
const userInfo = JSON.parse(sessionStorage.CDTPcookie);
// console.log(userInfo.userInfo.id,bindingValue)
if(bindingValue instanceof Array){
return bindingValue.some(v=>{
return userInfo.userInfo.id==v;
})
}
return userInfo.userInfo.id==bindingValue;
}
//角色控制
export function permissionByRole(bindingValue) {
//這里也可以是store里的用戶信息
const userInfo = JSON.parse(sessionStorage.userInfo);
let roles = []
if (userInfo) {
roles = userInfo.roleList
}
let show = false;
if (bindingValue instanceof Array) {
return roles.some(role => {//多角色處理
return bindingValue.some(item => {
return role.roleCode === item
})
})
} else if (typeof bindingValue == 'string') {
show = roles.some(role => {
return role.roleCode === bindingValue;
})
}
return show;
}
簡單說一下上面?指令的定義思路和使用方法。整體思路就是通過processingType處理權限邏輯,使用el.style.display控制組件顯示或隱藏。我在這里從日常應用中提取了一些通用的processingType中的權限處理方式,方便大家理解也供大家參考。
下面逐一說一下權限指令各個類型的使用方法:
//角色權限
<component v-permission:role='leader'></component>
//判斷當前登錄人
<component v-permission:currentUser='orderInfo.createUser'></component>
//判斷業務狀態
<component v-permission:bussinessStatus='{status:orderStatus.RUNNING,value:orderInfo.status}'></component>
//角色是leader或者是當前訂單的創建者,有權限
<component v-permission:some="{role:'leader',currentUser:'orderInfo.createUser'}"></component>
//角色是leader并且是當前訂單的創建者,有權限
<component v-permission:every="{role:'leader',currentUser:'orderInfo.createUser'}"></component>
v-input 輸入框限制,限制數字、保留n位小數點等。
export default {
inserted: function (el, binding, vnode) {
el.addEventListener('input', function (e) {
if (binding.arg == 'toFixed') {
//限制輸入n位小數點
toFiexd(e.target, vnode, binding.value)
} else {
//限制數字輸入
Integer(e.target, vnode)
}
})
},
}
function toFiexd(target, vnode, v) {
console.log(v);
let ln = 2;
if (v) {
ln = v;
}
var regStrs = [
['^0(\\d+)$', '$1'], //禁止錄入整數部分兩位以上,但首位為0
['[^\\d\\.]+$', ''], //禁止錄入任何非數字和點
['\\.(\\d?)\\.+', '.$1'], //禁止錄入兩個以上的點
['^(\\d+\\.\\d{' + ln + '}).+', '$1'] //禁止錄入小數點后兩位以上
];
for (var i = 0; i < regStrs.length; i++) {
var reg = new RegExp(regStrs[i][0]);
target.value = target.value.replace(reg, regStrs[i][1]);
}
//對于封裝的像el-input組件,因為其需要通過input事件同步狀態
if(vnode.componentInstance){
vnode.componentInstance.$listeners.input(target.value)
}
}
function Integer(target, vnode) {
let valueStr = target.value
if (valueStr.length == 1) {
//第一個數字不為0
valueStr = valueStr.replace(/[^0-9]/g, "");
} else {
//只能輸入正整數
valueStr = valueStr.replace(/\D/g, "");
}
target.value = valueStr;
if(vnode.componentInstance){
vnode.componentInstance.$listeners.input(target.value)
}
}
這里需要特別注意的是下面這行代碼
登錄后復制vnode.componentInstance.$listeners.input(target.value)
我們為什么需要添加這一句呢,我們明明已經為target.value做了賦值。
實際上這一句代碼相當于指令作用組件內部的$emit('input',target.value)
,這是因為如果我們是在antd或者elementui中的輸入框組件上添加我們定義的v-input指令,直接為target.value賦值是不能生效的,修改的只是原生input控件value值,并沒有修改自定義組件的value,還需要通過觸發input事件去同步組件狀態,修改value值。(這里不了解為什么需要觸發input事件區同步狀態的同學了解一下v-model的語法糖原理即可理解,
使用方法:
<!-- 限制輸入兩位小數數字 -->
<input v-input:toFixed="2"/>
<!-- 限制輸入正整數 -->
<el-input v-input:integer/>
我們也可以通過自定義指令做對內容到處理,比如
空值處理
數字千分數逗號分割
千分位分割代碼: 使用方法:export default {
bind:function(){
},
inserted:function(el,binding){
dealContent(el,binding)
},
update:function(el,binding){
dealContent(el,binding)
},
componentUpdated:function(){
},
unbind:function(){
},
}
function dealContent(el,binding){
const {arg}=binding;
if(arg=='empty'){
if(!el.textContent){//空值顯示
el.textContent=binding.value||'暫無數據';
}
}else if(arg=='money'){//金額千分位逗號分割,如10000000顯示為100,000,00
if (binding.value) {
el.textContent = dealMoney(binding.value);
}else {
el.textContent = dealMoney(el.textContent);
}
}
}
//金額處理
export function dealMoney(money, places = 2) {
const zero = `0.00`;
if (isNaN(money) || money === '') return zero;
if (money && money != null) {
money = `${money}`;
let left = money.split('.')[0]; // 小數點左邊部分
let right = money.split('.')[1]; // 小數點右邊
// 保留places位小數點,當長度沒有到places時,用0補足。
right = right ? (right.length >= places ? '.' + right.substr(0, places) : '.' + right + '0'.repeat(places - right.length)) : ('.' + '0'.repeat(places));
var temp = left.split('').reverse().join('').match(/(\d{1,3})/g); // 分割反向轉為字符串然后最多3個,最少1個,將匹配的值放進數組返回
return (Number(money) < 0 ? '-' : '') + temp.join(',').split('').reverse().join('') + right; // 補齊正負號和貨幣符號,數組轉為字符串,通過逗號分隔,再分割(包含逗號也分割)反向轉為字符串變回原來的順序
} else if (money === 0) {
return zero;
} else {
return zero;
}
}
<span v-content:empty="'無'">{{message}}</span>
<!-- 金額千分位逗號分割 -->
<span v-content:money>100000</span>
v-preview方便的實現文件預覽功能
預覽圖片;
預覽文件;
其他預覽類業務功能
使用方法:import {isOffic,isPdf,isImage} from '@/utils/base'
import {previewWithOffice} from '@/utils/fileUtils.js'
export default {
inserted:function(el,binding){
el.onclick=function(e){
let params = binding.value
if(isOffic(params.name)){
e.preventDefault()
e.stopPropagation()
previewWithOffice(params.url)//使用office在線預覽打開
}else if(isPdf(params.name) || isImage(params.name)){
e.preventDefault()
e.stopPropagation()
if(params.url){//直接打開url
previewFile(params)
}
}
}
},
//指令所在組件的 VNode 及其子 VNode 全部更新后
componentUpdated: function (el, binding) {
el.onclick=function(e){
let params = binding.value
if(isOffic(params.name)){
//使用插件預覽Office文件
e.preventDefault()
e.stopPropagation()
previewWithOffice(params.url)
}else if(isPdf(params.name) || isImage(params.name)){
//預覽圖片和pdf等能直接打開的文件
e.preventDefault()
e.stopPropagation()
previewFile(params)
}
}
},
unbind(el){
el.onclick=null;
}
}
//預覽圖片和pdf等能直接打開的文件
function previewFile(params) {
let a = document.createElement("a");
a.download = params.name
a.href = params.url;
a.target = "_blank";
a.click();
a = null;
}
<!-- 預覽圖片 -->
<image :src='url' v-preview="{name:file.name,url:file.url}"></image>
<!-- 預覽文件 -->
<span v-preview="{name:file.name,url:file.url}">{{file.name}}</span>
以上就是“Vue中的自定義指令怎么實現”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。