1. 程式人生 > 程式設計 >Vue 3.0自定義指令的使用入門

Vue 3.0自定義指令的使用入門

提示:在閱讀本文前,建議您先閱讀 vue 3 官方文件 自定義指令 章節的內容。

一、自定義指令

1、註冊全域性自定義指令

constapp=Vue.createApp({})

//註冊一個全域性自定義指令v-focus
app.directive('focus',{
//當被繫結的元素掛載到DOM中時被呼叫
mounted(el){
//聚焦元素
el.focus()
}
})

2、使用全域性自定義指令

<divid="app">
<inputv-focus/>
</div>

3、完整的使用示例

<divid="app">
<inputv-focus/>
</div>
<script>
const{createApp}=Vue

constapp=Vue.createApp({})//①
app.directive('focus',{//②
//當被繫結的元素掛載到DOM中時被呼叫
mounted(el){
el.focus()//聚焦元素
}
})
app.mount('#app')//③
</script>

當頁面載入完成後,頁面中的輸入框元素將自動獲得焦點。該示例的程式碼比較簡單,主要包含 3 個步驟:建立 App 物件、註冊全域性自定義指令和應用掛載。其中建立 App 物件的細節,阿寶哥會在後續的文章中單獨介紹,下面我們將重點分析其他 2 個步驟。首先我們先來分析註冊全域性自定義指令的過程。

二、註冊全域性自定義指令的過程

在以上示例中,我們使用 app 物件的 directive 方法來註冊全域性自定義指令:

app.directive('focus',{
//當被繫結的元素掛載到DOM中時被呼叫
mounted(el){
el.focus()//聚焦元素
}
})

當然,除了註冊全域性自定義指令外,我們也可以註冊區域性指令,因為元件中也接受一個 directives 的選項:

directives:{
focus:{
mounted(el){
el.focus()
}
}
}

對於以上示例來說,我們使用的 app.directive 方法被定義在 runtime-core/src/apiCreateApp.ts 檔案中:

//packages/runtime-core/src/apiCreateApp.ts
exportfunctioncreateAppAPI<HostElement>(
render:RootRenderFunction,hydrate?:RootHydrateFunction
):CreateAppFunction<HostElement>{
returnfunctioncreateApp(rootComponent,rootProps=null){
constcontext=createAppContext()
letisMounted=false

constapp:App=(context.app={
//省略部分程式碼
_context:context,//用於註冊或檢索全域性指令。
directive(name:string,directive?:Directive){
if(__DEV__){
validateDirectiveName(name)
}
if(!directive){
returncontext.directives[name]asany
}
if(__DEV__&&context.directives[name]){
warn(`Directive"${name}"hasalreadybeenregisteredintargetapp.`)
}
context.directives[name]=directive
returnapp
},returnapp
}
}

通過觀察以上程式碼,我們可以知道 directive 方法支援以下兩個引數:

  • name:表示指令的名稱;
  • directive(可選):表示指令的定義。

name 引數比較簡單,所以我們重點分析 directive 引數,該引數的型別是 Directive 型別:

//packages/runtime-core/src/directives.ts
exporttypeDirective<T=any,V=any>=
|ObjectDirective<T,V>
|FunctionDirective<T,V>

由上可知 Directive 型別屬於聯合型別,所以我們需要繼續分析 ObjectDirective 和 FunctionDirective 型別。這裡我們先來看一下 ObjectDirective 型別的定義:

//packages/runtime-core/src/directives.ts
exportinterfaceObjectDirective<T=any,V=any>{
created?:DirectiveHook<T,null,&nbswww.cppcns.comp;V>
beforeMount?:DirectiveHook<T,V>
mounted?:DirectiveHook<T,V>
beforeUpdate?:DirectiveHook<T,VNode<any,T>,V>
updated?:DirectiveHook<T,V>
beforeUnmount?:DirectiveHook<T,V>
unmounted?:DirectiveHook<T,V>
getSSRProps?:SSRDirectiveHook
}

該型別定義了物件型別的指令,物件上的每個屬性表示指令生命週期上的鉤子。而 FunctionDirective 型別則表示函式型別的指令:

//packages/runtime-core/src/directives.ts
exporttypeFunctionDirective<T=any,V=any>=DirectiveHook<T,any,V>

exporttypeDirectiveHook<T=any,Prev=VNode<any,T>|null,V=any>=(
el:T,binding:DirectiveBinding<V>,vnode:VNode<any,prevVNode:Prev
)=>void

介紹完 Directive 型別,我們再回顧一下前面的示例,相信你就會清晰很多:

app.directive('focus',{
//當被繫結的元素掛載到DOM中時觸發
mounted(el){
el.focus()//聚焦元素
}
})

對於以上示例,當我們呼叫 app.directive 方法註冊自定義 focus 指令時,就會執行以下邏輯:

directive(name:string,directive?:Directive){
if(__DEV__){//避免自定義指令名稱,與已有的內建指令名稱衝突
validateDirectiveName(name)
}
if(!directive){//獲取name對應的指令物件
returncontext.direhttp://www.cppcns.comctives[name]asany
}
if(__DEV__&&context.directives[name]){
warn(`Directive"${name}"hasalreadybeenregisteredintargetapp.`)
}
context.directives[name]=directive//註冊全域性指令
returnapp
}

當 focus 指令註冊成功之後,該指令會被儲存在 context 物件的 directives 屬性中,具體如下圖所示:

Vue 3.0自定義指令的使用入門

顧名思義 context 是表示應用的上下文物件,那麼該物件是如何建立的呢?其實,該物件是通過 createAppContext 函式來建立的:

constcontext=createAppContext()

而 createAppContext 函式被定義在 runtime-core/src/apiCreateApp.ts 檔案中:

//packages/runtime-core/src/apiCreateApp.ts
exportfunctioncreateAppContext():AppContext{
return{
app:nullasany,config:{
isNativeTag:NO,performance:false,globalProperties:{},optionMergeStrategies:{},isCustomElement:NO,errorHandler:undefined,warnHandler:undefined
},mixins:[],components:{},directives:{},provides:Object.create(null)
}
}

看到這裡,是不是覺得註冊全域性自定義指令的內部處理邏輯其實挺簡單的。那麼對於已註冊的 focus 指令,何時會被呼叫呢?要回答這個問題,我們就需要分析另一個步驟 —— 應用掛載。

三、應用掛載的過程

為了更加直觀地瞭解應用掛載的過程,阿寶哥利用 Chrome 開發者工具,記錄了應用掛載的主要過程:

Vue 3.0自定義指令的使用入門

通過上圖,我們就可以知道應用掛載期間所經歷的主要過程。此外,從圖中我們也發現了一個與指令相關的函式 resolveDirective。很明顯,該函式用於解析指令,且該函式在 render 方法中會被呼叫。在原始碼中,我們找到了該函式的定義:

//packages/runtime-core/src/helpers/resolveAssets.ts
exportfunctionresolveDirective(name:string):Directive|undefined{
returnresolveAsset(DIRECTIVES,name)
}

在 resolveDirective 函式內部,會繼續呼叫 resolveAsset 函式來執行具體的解析操作。在分析 resolveAsset 函式的具體實現之前,我們在 resolveDirective 函式內部加個斷點,來一睹 render 方法的 “芳容”:

Vue 3.0自定義指令的使用入門

在上圖中,我們看到了與 focus 指令相關的 _resolveDirective("focus") 函式呼叫。前面我們已經知道在 resolveDirective 函式內部會繼續呼叫 resolveAsset 函式,該函式的具體實現如下:

//packages/runtime-core/src/helpers/resolveAssets.ts
functionresolveAsset(
type:typeofCOMPONENTS|typeofDIRECTIVES,name:string,warnMissing=true
){
constinstance=currentRenderingInstance||currentInstance
if(instance){
constComponent=instance.type
//省略解析元件的處理邏輯
constres=
//區域性註冊
resolve(instance[type]||(ComponentasComponentOptions)[type],name)||
//全域性註冊
resolve(instance.appContext[type],name)
returnres
}elseif(__DEV__){
warn(
`resolve${capitalize(type.slice(0,-1))}`+
`canonlybeusedinrender()orsetup().`
)
}
}

因為註冊 focus 指令時,使用的是全域性註冊的方式,所以解析的過程會執行 resolve(instance.appContext[type],name) 該語句,其中 resolve 方法的定義如下:

functionresolve(registry:Record<string,any>|undefined,name:string){
return(
registry&&
(registry[name]||
registry[camelize(name)]||
registry[capitalize(camelize(name))])
)
}

分析完以上的處理流程,我們可以知道在解析全域性註冊的指令時,會通過 resolve 函式從應用的上下文物件中獲取已註冊的指令物件。在獲取到 _directive_focus 指令物件後,render 方法內部會繼續呼叫 _withDirectives 函式,用於把指令新增到 VNode 物件上,該www.cppcns.com函式被定義在 runtime-core/src/directives.ts 檔案中:

//packages/runtime-core/src/directives.ts
exportfunctionwithDirectives<TextendsVNode>(
vnode:T,directives:DirectiveArguments
):T{
constinternalInstance=currentRenderingInstance//獲取當前渲染的例項
constinstance=internalInstance.proxy
constbindings:DirectiveBinding[]=vnode.dirs||(vnode.dirs=[])
for(leti=0;i<directives.length;i++){
let[dir,value,arg,modifiers=EMPTY_OBJ]=directives[i]
//在mounted和updated時,觸發相同行為,而不關係其他的鉤子函式
if(isFunction(dir)){//處理函式型別指令
dir={
mounted:dir,updated:dir
}asObjectDirective
}
bindings.push({
dir,instance,oldValue:void0,modifiers
})
}
returnvnode
}

因為一個節點上可能會應用多個指令,所以 withDirectives 函式在 VNode 物件上定義了一個 dirs 屬性且該屬性值為陣列。對於前面的示例來說,在呼叫 withDirectives 函式之後,VNode 物件上就會新增一個 dirs 屬性,具體如下圖所示:

Vue 3.0自定義指令的使用入門

通過上面的分析,我們已經知道在元件的 render 方法中,我們會通過 withDirectives 函式把指令註冊對應的 VNode 物件上。那麼 focus 指令上定義的鉤子什麼時候會被呼叫呢?在繼續分析之前,我們先來介紹一下指令物件所支援的鉤子函式。

一個指令定義物件可以提供如下幾個鉤子函式 (均為可選):

  • created:在繫結元素的屬性或事件監聽器被應用之前呼叫。
  • beforeMount:當指令第一次繫結到元素並且在掛載父元件之前呼叫。
  • mounted:在繫結元素的父元件被掛載後呼叫。
  • beforeUpdate:在更新包含元件的 VNode 之前呼叫。
  • updated:在包含元件的 VNode 及其子元件的 VNode 更新後呼叫。
  • beforeUnmount:在解除安裝繫結元素的父元件之前呼叫。
  • unmounted:當指令與元素解除繫結且父元件已解除安裝時,只調用一次。

介紹完這些鉤子函式之後,我們再來回顧一下前面介紹的 ObjectDirective 型別:

//packages/runtime-core/src/directives.ts
exportinterfaceObjectDirective<T=any,V>
beforeMount?:DirectiveHook<T,V>
getSSRProps?:SSRDirectiveHook

好的,接下來我們來分析一下 focus 指令上定義的鉤子什麼時候被呼叫。同樣,阿寶哥在 focus 指令的 mounted 方法中加個斷點:

Vue 3.0自定義指令的使用入門

在圖中右側的呼叫棧中,我們看到了 invokeDirectiveHook 函式,很明顯該函式的作用就是呼叫指令上已註冊的鉤子。出於篇幅考慮,具體的細節阿寶哥就不繼續介紹了,感興趣的小夥伴可以自行斷點除錯一下。

四、阿寶哥有話說

4.1 Vue 3 有哪些內建指令?

在介紹註冊全域性自定義指令的過程中,我們看到了一個 validateDirectiveName 函式,該函式用於驗證自定義指令的名稱,從而避免自定義指令名稱,與已有的內建指令名稱衝突。

//packages/runtime-core/src/directives.ts
exportfunctionvalidateDirectiveName(name:string){
if(isBuiltInDirective(name)){
warn('Donotusebuilt-indirectiveidsascustomdirectiveid:'+name)
}
}

在 validateDirectiveName 函式內部,會通過 isBuiltInDirective(name) 語句來判斷是否為內建指令:

constisBuiltInDirective=/*#__PURE__*/makeMap(
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text'
)

以上程式碼中的 makeMap 函式,用於生成一個 map 物件(Object.create(null))並返回一個函式,用於檢測某個 key 是否存在 map 物件中。另外,通過以上程式碼,我們就可以很清楚地瞭解 Vue 3 中為我們提供了哪些內建指令。

4.2 指令有幾種型別?

在 Vue 3 中指令分為 ObjectDirective 和 FunctionDirective 兩種型別:

//packages/runtime-core/src/directives.ts
exporttypeDirective<T=any,V>

ObjectDirective

exportinterfaceObjectDirective<T=any,V>
getSSRProps?:SSRDirectiveHook
}

FunctionDirective

exporttypeFunctionDirective<T=any,prevVNode:Prev
)=>void

如果你想在 mounted 和 updated 時觸發相同行為,而不關心其他的鉤子函式。那麼你可以通過將回調函式傳遞給指令來實現

app.directive('pin',(el,binding)=>{
el.style.position='fixed'
consts=binding.arg||'top'
el.style[s]=binding.value+'px'
})

4.3 註冊全域性指令與區域性指令有什麼區別?

註冊全域性指令

app.directive('focus',{
//當被繫結的元素掛載到DOM中時被呼叫
mounted(el){
el.focus()//聚焦元素
}
});

註冊區域性指令

constComponent=defineComponent({
directives:{
focus:{
mounted(el){
el.focus()
}
}
},render(){
const{directives}=this.$options;
return[withDirectives(h('input'),[[directives.focus,]])]
}
});

解析全域性註冊和區域性註冊的指令

//packages/runtime-core/src/helpers/resolveAssets.ts
functionresolveAsset(
type:typeofCOMPONENTS|typeofDIRECTIVES,warnMissing=true
)&nbswww.cppcns.comp;{
constinstance=currentRenderingInstance||currentInstance
if(instance){
constComponent=instance.type
//省略解析元件的處理邏輯
constres=
//區域性註冊
resolve(instance[type]||(ComponentasComponentOptions)[type],name)
returnres
}
}

4.4 內建指令和自定義指令生成的渲染函式有什麼區別?

要了解內建指令和自定義指令生成的渲染函式的區別,阿寶哥以 v-if 、v-show 程式設計客棧內建指令和 v-focus 自定義指令為例,然後使用 Vue 3 Template Explorer 這個線上工具來編譯生成渲染函式:

v-if 內建指令

<inputv-if="isShow"/>

const_Vue=Vue
returnfunctionrender(_ctx,_cache,$props,$setup,$data,$options){
with(_ctx){
const{createVNode:_createVNode,openBlock:_openBlock,createBlock:_createBlock,createCommentVNode:_createCommentVNode}=_Vue

returnisShow
?(_openBlock(),_createBlock("input",{key:0}))
:_createCommentVNode("v-if",true)
}
}

對於 v-if 指令來說,在編譯後會通過 ?: 三目運算子來實現動態建立節點的功能。

v-show 內建指令

<inputv-show="isShow"/>

const_Vue=Vue
returnfunctionrender(_ctx,$options){
with(_ctx){
const{vShow:_vShow,createVNode:_createVNode,withDirectives:_withDirectives,createBlock:_createBlock}=_Vue

return_withDirectives((_openBlock(),512/*NEED_PATCH*/)),[
[_vShow,isShow]
])
}
}

以上示例中的 vShow 指令被定義在 packages/runtime-dom/src/directives/vShow.ts 檔案中,該指令屬於 ObjectDirective 型別的指令,該指令內部定義了 beforeMount、mounted、updated 和 beforeUnmount 四個鉤子。

v-focus 自定義指令

<inputv-focus/>

const_Vue=Vue
returnfunctionrender(_ctx,$options){
with(_ctx){
const{resolveDirective:_resolveDirective,createBlock:_createBlock}=_Vue

const_directive_focus=_resolveDirective("focus")
return_withDirectives((_openBlock(),[
[_directive_focus]
])
}
}

通過對比 v-focus 與 v-show 指令生成的渲染函式,我們可知 v-focus 自定義指令與 v-show 內建指令都會通過 withDirectives 函式,把指令註冊到 VNode 物件上。而自定義指令相比內建指令來說,會多一個指令解析的過程。

此外,如果在 input 元素上,同時應用了 v-show 和 v-focus 指令,則在呼叫 _withDirectives 函式時,將使用二維陣列:

<inputv-show="isShow"v-focus/>

const_Vue=Vue
returnfunctionrender(_ctx,resolveDirective:_resolveDirective,isShow],[_directive_focus]
])
}
}

4.5 如何在渲染函式中應用指令?

除了在模板中應用指令之外,利用前面介紹的 withDirectives 函式,我們可以很方便地在渲染函式中應用指定的指令:

<divid="app"></div>
<script>
const{createApp,h,vShow,defineComponent,withDirectives}=Vue
constComponent=defineComponent({
data(){
return{value:true}
},render(){
return[withDirectives(h('div','我是阿寶哥'),[[vShow,this.value]])]
}
});
constapp=Vue.createApp(Component)
app.mount('#app')
</script>

本文阿寶哥主要介紹了在 Vue 3 中如何自定義指令、如何註冊全域性和區域性指令。為了讓大家能夠更深入地掌握自定義指令的相關知識,阿寶哥從原始碼的角度分析了指令的註冊和應用過程。

在後續的文章中,阿寶哥將會介紹一些特殊的指令,當然也會重點分析一下雙向繫結的原理,感興趣的小夥伴不要錯過喲。

以上就是Vue 3.0自定義指令的使用入門的詳細內容,更多關於Vue 3.0自定義指令的使用的資料請關注我們其它相關文章!