通常我們在 Vue 中使用組件,是像這樣在模板中寫標簽:
<MyComponent :prop="value" @event="handleEvent" />
而函數式調用,則是讓我們像調用一個普通 JavaScript 函數一樣來使用這個組件,例如:
MyComponentFunction({ prop: value }).then(result => { /* ... */ })。
接下來我們就用一個實際的例子來看看這種函數式調用的寫法是怎么寫的。
我們來實現一個非常通用的功能,在系統中,如果某些操作需要進行身份驗證才能進行下一步,我們就需要實現一個身份驗證的彈框,只有驗證了用戶的賬號密碼的情況下,才能執行接下來的邏輯。
以下是這個AuthBox
組件的部分,這里無需多言。
// src/components/AuthBox/src/AuthBox.vue
<template><el-dialogtitle="身份驗證"v-model="state.dialogVisible"width="360px":custom-class="customClass"centeralign-centerdestroy-on-close:show-close="false":close-on-click-modal="false"@opened="handleOpened"@closed="handleClosed"><el-form ref="formRef" :model="state.formData" :rules="formRules" label-width="70px" :validate-on-rule-change="false"><el-form-item label="賬號" prop="username"><el-inputref="usernameRef"v-model="state.formData.username"placeholder="請輸入賬號"@keyup.enter="handlePasswordFocus"/></el-form-item><el-form-item label="密碼" prop="password"><el-inputref="passwordRef"v-model="state.formData.password"type="password"placeholder="請輸入密碼"@keyup.enter="handleConfirm"/></el-form-item></el-form><template #footer><div class="text-center"><el-button @click="handleCancel">取消</el-button><el-button type="primary" @click="handleConfirm">確認</el-button></div></template></el-dialog>
</template><script lang="ts" setup>
import { ref, reactive, nextTick, onMounted } from "vue";
import { ElDialog, ElForm, ElFormItem, ElInput, ElButton, ElMessage } from "element-plus";
import { AuthBoxState } from "./type";// 定義組件屬性
const props = defineProps({// 自定義類名customClass: {type: String,default: ""},// 提示文本message: {type: String,default: ""}
});// 定義事件
const emits = defineEmits(["confirm", "cancel", "close", "vanish"]);// 組件狀態
const state = reactive<AuthBoxState>({dialogVisible: false,formData: {username: "",password: ""}
});// 表單校驗規則
const formRules = {username: [{ required: true, message: "請輸入賬號", trigger: "blur" }],password: [{ required: true, message: "請輸入密碼", trigger: "blur" }]
};// 表單引用
const formRef = ref();
const usernameRef = ref();
const passwordRef = ref();// 聚焦密碼輸入框
const handlePasswordFocus = () => {passwordRef.value.focus();
};// 確認按鈕處理
const handleConfirm = () => {formRef.value.validate((valid: boolean) => {if (valid) {// 在實際應用中,這里可能會調用API進行驗證// 這里我們簡化為直接模擬驗證通過// 觸發confirm事件,傳遞表單數據emits("confirm", {username: state.formData.username,password: state.formData.password});// 關閉對話框state.dialogVisible = false;}});
};// 取消按鈕處理
const handleCancel = () => {emits("cancel");state.dialogVisible = false;
};// 對話框打開后處理
const handleOpened = () => {// 對話框完全打開后才設置焦點,確保元素已渲染完成并可見usernameRef.value?.focus();
};// 對話框關閉處理
const handleClosed = () => {emits("close");// 組件消失,用于清理資源nextTick(() => {emits("vanish");});
};// 公開給外部的關閉方法
const doClose = () => {state.dialogVisible = false;
};// 初始化
const init = () => {// 重置表單state.formData.username = "";state.formData.password = "";// 顯示對話框state.dialogVisible = true;
};// 組件掛載時初始化
onMounted(() => {init();
});// 暴露方法給父組件調用
defineExpose({doClose,state
});
</script>
接下來重點看看關于函數式調用的部分:
// src/components/AuthBox/index.tsimport { AppContext, ComponentPublicInstance, createVNode, render } from "vue";
import AuthBoxConstructor from "./src/AuthBox.vue";
import { AuthBoxData, AuthBoxOptions, AuthBoxState, Callback, IAuthBox } from "./src/type";/*** 實例映射表 - 存儲所有通過函數式調用創建的AuthBox實例** Key: 組件實例的代理對象(vm),包含doClose方法* Value: 包含options、callback、Promise的resolve和reject函數** 作用: 讓我們能夠在異步事件(如用戶點擊確認)發生時,找到對應的Promise并解析它*/
const instanceMap = new Map<ComponentPublicInstance<{ doClose: () => void }>,{options: AuthBoxOptions;callback: Callback | undefined;resolve: (res: any) => void;reject: (reason?: any) => void;}
>();/*** 獲取組件應該掛載到的DOM元素** @param props - 組件的props* @returns 掛載目標DOM元素,默認為document.body*/
const getAppendToElement = (props: AuthBoxOptions): HTMLElement => {// 這里簡化處理,始終返回document.body// 在實際應用中,可以根據props.appendTo來自定義掛載位置return document.body;
};/*** 創建臨時容器元素** @returns 新創建的div元素*/
const genContainer = (): HTMLDivElement => {return document.createElement("div");
};/*** 初始化組件實例** @param props - 傳遞給組件的屬性* @param container - 臨時容器元素* @param appContext - Vue應用上下文(可選)* @returns 創建的組件實例*/
const initInstance = (props: AuthBoxOptions, container: HTMLElement, appContext: AppContext | null = null) => {// 1. 使用組件構造函數和props創建虛擬節點const vnode = createVNode(AuthBoxConstructor, props);// 2. 如果提供了應用上下文,則設置到vnode上// (這確保組件能訪問到應用的全局組件、插件等)if (appContext) {vnode.appContext = appContext;}// 3. 將虛擬節點渲染到臨時容器中render(vnode, container);// 4. 將容器中渲染好的DOM元素移動到目標掛載點(通常是body)getAppendToElement(props).appendChild(container.firstElementChild!);// 5. 返回組件實例return vnode.component;
};/*** 顯示AuthBox對話框** @param options - 配置選項* @param appContext - 應用上下文(可選)* @returns 創建的組件實例代理對象*/
const showAuthBox = (options: AuthBoxOptions, appContext?: AppContext | null) => {// 1. 創建臨時容器const container = genContainer();// 2. 設置組件銷毀時的回調// 當組件通過transition動畫完全消失后觸發options.onVanish = () => {// 2.1 從DOM中徹底移除組件// (將null渲染到container會清除其中的內容)render(null, container);// 2.2 從實例映射表中移除組件實例// (防止內存泄漏)instanceMap.delete(vm);};// 3. 設置用戶點擊確認按鈕的回調options.onConfirm = (userData: { username: string; password: string }) => {// 3.1 獲取該組件實例對應的Promise解析函數const currentInstance = instanceMap.get(vm)!;// 3.2 創建返回數據const resolveData: AuthBoxData = {username: userData.username,password: userData.password,action: "confirm"};// 3.3 解析Promise,傳遞結果數據// (這會使得await AuthBox()或.then()收到結果)currentInstance.resolve(resolveData);};// 4. 設置用戶點擊取消按鈕的回調options.onCancel = () => {const currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = {username: "",password: "",action: "cancel"};currentInstance.resolve(resolveData);};// 5. 設置對話框關閉的回調options.onClose = () => {const currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = {username: "",password: "",action: "close"};currentInstance.resolve(resolveData);};// 6. 初始化并創建組件實例const instance = initInstance(options, container, appContext)!;// 7. 獲取組件實例的代理對象// (這是我們與組件交互的接口)const vm = instance.proxy as ComponentPublicInstance<{doClose: () => void;} & AuthBoxState>;// 8. 返回代理對象return vm;
};/*** AuthBox函數 - 用于函數式調用AuthBox組件** 用法:* const result = await AuthBox({ title: '登錄', message: '請輸入您的賬號和密碼' });* if (result.action === 'confirm') {* console.log('用戶名:', result.username);* console.log('密碼:', result.password);* }** @param options - AuthBox配置選項* @param appContext - Vue應用上下文(可選)* @returns Promise,解析為AuthBoxData*/
async function AuthBox(options: AuthBoxOptions, appContext?: AppContext | null): Promise<AuthBoxData>;
function AuthBox(options: AuthBoxOptions, appContext: AppContext | null = null): Promise<AuthBoxData> {// 1. 創建并返回一個新的Promise// (這是函數式調用的核心,讓我們可以使用await或.then()獲取結果)return new Promise((resolve, reject) => {// 2. 獲取應用上下文(優先使用傳入的,否則使用預設的)const finalAppContext = appContext ?? (AuthBox as IAuthBox)._context;// 3. 顯示AuthBox對話框,獲取組件實例代理const vm = showAuthBox(options, finalAppContext);// 4. 將組件實例與Promise的resolve/reject函數關聯起來// (這樣在事件回調中就能找到對應的Promise進行解析)instanceMap.set(vm, {options,callback: undefined, // 保留字段,便于擴展resolve,reject});});
}/*** 關閉所有通過AuthBox函數創建的對話框*/
AuthBox.close = () => {// 1. 遍歷實例映射表中的所有組件實例instanceMap.forEach((_, vm) => {// 2. 調用每個實例的doClose方法關閉對話框vm.doClose();});// 3. 清空實例映射表// (作為安全措施,確保不留下任何引用)instanceMap.clear();
};// 初始化應用上下文為null
(AuthBox as IAuthBox)._context = null;// 導出函數
export default AuthBox as IAuthBox;
下面我們就重點分析看一下,這個 indes.ts
做了什么:
- 導入依賴
import { AppContext, ComponentPublicInstance, createVNode, render } from "vue";
import AuthBoxConstructor from "./src/AuthBox.vue";
import { AuthBoxData, AuthBoxOptions, AuthBoxState, Callback, IAuthBox } from "./src/type";
vue
:從 Vue 中導入了核心的AppContext
(應用上下文)、ComponentPublicInstance
(組件公共實例類型)、createVNode
(創建虛擬DOM節點) 和render
(渲染虛擬DOM) 函數。AuthBoxConstructor
:這是實際的AuthBox.vue
組件。我們將其作為構造函數來創建組件實例。./src/type
:從類型定義文件中導入了與AuthBox
組件相關的各種類型,這里就不放出來了,根據自己實際業務來寫就行。
instanceMap
:實例映射表
const instanceMap = new Map<ComponentPublicInstance<{ doClose: () => void }>,{options: AuthBoxOptions;callback: Callback | undefined;resolve: (res: any) => void;reject: (reason?: any) => void;}
>();
- 作用:這是一個關鍵的數據結構。由于我們可以通過函數調用創建多個
AuthBox
實例,instanceMap
用于存儲每一個動態創建的AuthBox
組件實例 (vm
) 以及與之關聯的配置項 (options
) 和 Promise 的resolve
/reject
函數。 - 鍵 (Key):
ComponentPublicInstance<{ doClose: () => void }>
,表示AuthBox
組件的實例代理對象。這個代理對象上預期有一個doClose
方法,用于關閉對話框。 - 值 (Value):一個對象,包含:
options
: 調用AuthBox
時傳入的配置。callback
: 一個可選的回調函數 (這里標記為undefined
,保留了擴展性)。resolve
: Promise 的resolve
函數。當用戶在AuthBox
中完成操作 (如點擊確認) 時,我們會調用這個函數來解決 (fulfill) Promise,并傳遞結果。reject
: Promise 的reject
函數。如果發生錯誤或需要中斷操作,會調用此函數。
- 為什么需要它? 當
AuthBox
組件內部發生事件 (如用戶點擊按鈕) 時,我們需要一種方式找到當初調用它時創建的那個 Promise,以便能將結果傳遞回去。instanceMap
就是通過組件實例這個橋梁來找到對應的 Promise 控制函數的。
getAppendToElement
:獲取掛載目標
const getAppendToElement = (props: AuthBoxOptions): HTMLElement => {return document.body;
};
- 作用:決定
AuthBox
組件的 DOM 元素最終應該被插入到頁面的哪個位置。 - 實現:這里簡化了處理,固定返回
document.body
,AuthBox
會被掛載到<body>
元素的末尾。 - 擴展性:在實際應用中,可以根據需要來自定義掛載位置。
genContainer
:創建臨時容器
const genContainer = (): HTMLDivElement => {return document.createElement("div");
};
- 作用:創建一個臨時的
<div>
元素。 - 為什么需要臨時容器? Vue 的
render
函數需要一個容器元素來渲染虛擬節點。我們先將組件渲染到這個臨時容器中,然后再將容器內的實際 DOM 元素(即AuthBox
的根元素)移動到由getAppendToElement
指定的最終掛載點。
initInstance
:初始化組件實例
const initInstance = (props: AuthBoxOptions, container: HTMLElement, appContext: AppContext | null = null) => {// 1. 使用組件構造函數和props創建虛擬節點const vnode = createVNode(AuthBoxConstructor, props);// 2. 如果提供了應用上下文,則設置到vnode上if (appContext) {vnode.appContext = appContext;}// 3. 將虛擬節點渲染到臨時容器中render(vnode, container);// 4. 將容器中渲染好的DOM元素移動到目標掛載點(通常是body)getAppendToElement(props).appendChild(container.firstElementChild!);// 5. 返回組件實例return vnode.component;
};
- 作用:這個函數負責創建
AuthBox
組件的 Vue 實例并將其渲染到 DOM 中。 - 步驟:
- 創建虛擬節點 (VNode):使用
createVNode(AuthBoxConstructor, props)
。AuthBoxConstructor
是導入的.vue
文件,props
是傳遞給組件的屬性。 - 設置應用上下文 (
appContext
):如果調用時傳入了appContext
,則將其設置到vnode.appContext
。它能確保動態創建的組件實例可以訪問到主 Vue 應用實例中注冊的全局組件、指令、插件以及 provide/inject 等。 - 渲染到臨時容器:調用
render(vnode, container)
,將虛擬節點轉換成真實的 DOM 元素,并插入到container
(由genContainer
創建的div
)中。 - 移動到最終掛載點:
getAppendToElement(props).appendChild(container.firstElementChild!)
。這一步是將container
中的第一個子元素 (即AuthBox
組件的根 DOM 元素) 移動到document.body
(或getAppendToElement
返回的其他元素) 中。 - 返回組件實例:
vnode.component
是實際的 Vue 組件實例對象,我們可以通過它訪問組件的屬性和方法。
- 創建虛擬節點 (VNode):使用
showAuthBox
:顯示對話框并處理回調
const showAuthBox = (options: AuthBoxOptions, appContext?: AppContext | null) => {const container = genContainer(); // 1. 創建臨時容器// 2. 設置組件銷毀時的回調options.onVanish = () => { // Linter Error: Property 'onVanish' does not exist on type 'AuthBoxOptions'.render(null, container); // 從DOM中徹底移除instanceMap.delete(vm); // 從實例映射表中移除};// 3. 設置用戶點擊確認按鈕的回調options.onConfirm = (userData: { username: string; password: string }) => { // Linter Errorconst currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = { /* ... */ action: "confirm" };currentInstance.resolve(resolveData);};// 4. 設置用戶點擊取消按鈕的回調options.onCancel = () => { // Linter Errorconst currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = { /* ... */ action: "cancel" };currentInstance.resolve(resolveData);};// 5. 設置對話框關閉的回調 (例如點擊遮罩層或右上角關閉按鈕)options.onClose = () => { // Linter Errorconst currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = { /* ... */ action: "close" };currentInstance.resolve(resolveData);};const instance = initInstance(options, container, appContext)!;const vm = instance.proxy as ComponentPublicInstance< /* ... */ >; // 7. 獲取組件實例代理return vm; // 8. 返回代理對象
};
- 作用:這是實際處理
AuthBox
顯示邏輯和事件回調的核心函數。 - 步驟與解釋:
- 創建容器:調用
genContainer()
。 - 設置
options.onVanish
:- 這個回調函數會在
AuthBox
組件從界面上完全消失時被調用。 - 內部邏輯:
render(null, container)
: 這是 Vue 中卸載組件并從 DOM 中移除其內容的方法。instanceMap.delete(vm)
: 從instanceMap
中刪除對此實例的引用,防止內存泄漏。
- 這個回調函數會在
- 設置
options.onConfirm
:- 當用戶在
AuthBox
組件內部點擊“確認”按鈕時,AuthBox
組件會觸發這個回調,并傳入用戶數據 (userData
)。 - 內部邏輯:
instanceMap.get(vm)!
: 通過組件實例vm
從instanceMap
中獲取之前存儲的 Promiseresolve
函數。- 構造
resolveData
:包含用戶名、密碼和操作類型 (action: "confirm"
)。 currentInstance.resolve(resolveData)
: 調用 Promise 的resolve
函數,將resolveData
作為結果傳遞出去。這將使得等待此 Promise 的await AuthBox(...)
調用得到結果。
- 當用戶在
- 設置
options.onCancel
:- 當用戶點擊“取消”按鈕時觸發。
- 邏輯與
onConfirm
類似,但action
為"cancel"
,并且通常不包含用戶輸入數據。
- 設置
options.onClose
:- 當對話框因其他方式關閉(如點擊遮罩層、按下 Esc 鍵,或組件內部調用關閉邏輯)時觸發。
- 邏輯與
onCancel
類似,action
為"close"
。
- 初始化實例:調用
initInstance(options, container, appContext)
創建并掛載AuthBox
組件。注意,這里的options
對象已經被我們動態添加了onVanish
,onConfirm
,onCancel
,onClose
這些回調處理函數。AuthBox.vue
組件內部在合適的時機(如用戶點擊、組件卸載)就會調用這些通過props
傳遞進來的函數。 - 獲取組件代理
vm
:instance.proxy
是組件實例的代理對象,通過它來調用組件的方法或訪問其數據 (如果組件暴露了doClose
方法和AuthBoxState
中的狀態)。 - 返回代理對象
vm
:將vm
返回。
- 創建容器:調用
AuthBox
主函數 (函數式調用的入口)
async function AuthBox(options: AuthBoxOptions, appContext?: AppContext | null): Promise<AuthBoxData>;
function AuthBox(options: AuthBoxOptions, appContext: AppContext | null = null): Promise<AuthBoxData> {return new Promise((resolve, reject) => {const finalAppContext = appContext ?? (AuthBox as IAuthBox)._context;const vm = showAuthBox(options, finalAppContext);instanceMap.set(vm, {options,callback: undefined,resolve,reject});});
}
- 作用:這是最終暴露給用戶調用的函數。它使得我們可以像
const result = await AuthBox({ });
這樣使用。 - 實現邏輯:
- 返回
Promise
:核心在于return new Promise((resolve, reject) => { ... });
。這使得AuthBox
函數的調用結果可以被await
或者通過.then()
方法處理。 - 確定應用上下文:
const finalAppContext = appContext ?? (AuthBox as IAuthBox)._context;
- 優先使用調用時直接傳入的
appContext
。 - 如果沒傳,則嘗試使用
(AuthBox as IAuthBox)._context
。這是一個預設的或全局設置的appContext
(見后續解釋)。
- 優先使用調用時直接傳入的
- 顯示
AuthBox
:const vm = showAuthBox(options, finalAppContext);
調用我們之前定義的showAuthBox
函數,傳入用戶配置和確定的應用上下文。這將創建、掛載組件,并返回組件實例代理vm
。 - 存儲到
instanceMap
:instanceMap.set(vm, { options, callback: undefined, resolve, reject });
- 它將當前創建的組件實例
vm
與新創建的 Promise 的resolve
和reject
函數關聯起來,并存儲到instanceMap
中。 - 這樣,當
showAuthBox
中設置的onConfirm
,onCancel
,onClose
等回調被AuthBox.vue
組件內部觸發時,它們可以通過vm
從instanceMap
中找到對應的resolve
(或reject
) 函數,從而完成 Promise,將數據傳遞給AuthBox
函數的調用者。
- 它將當前創建的組件實例
- 返回
AuthBox.close
:關閉所有實例
AuthBox.close = () => {instanceMap.forEach((_, vm) => {vm.doClose(); // 調用每個實例的doClose方法});instanceMap.clear(); // 清空映射表
};
- 作用:提供一個靜態方法,用于關閉所有當前通過
AuthBox
函數打開的對話框實例。 - 實現:
- 遍歷
instanceMap
中的所有組件實例代理 (vm
)。 - 調用每個
vm
上的doClose()
方法。這要求AuthBox.vue
組件通過defineExpose
暴露了一個名為doClose
的方法,用于執行關閉自身的邏輯(比如設置一個內部狀態讓組件隱藏,然后觸發過渡動畫,最終觸發onVanish
)。 instanceMap.clear()
: 清空映射表。
- 遍歷
以上就是這個 index.ts 的詳細解釋,相信你能明白vue中的組件是如何通過函數式調用的了,這樣的調用方式非常的方便。