在前幾篇文章中我們一起學習了 Vue3 中新穎的 Composition API,而今天筆者要帶大家一起看一下 Vue3 中的另一個新鮮的寫法 —— setup。
在絕大多數情況,我們書寫的組件都是有狀態的組件,而這類組件在初始化的過程中會被標記爲 stateful comonents,當 Vue3 檢測到我們在處理這類有狀態組件時,就會調用函數 setupStatefulComponent ,來初始化一個狀態化組件。處理組件部分的源碼位置在: @vue/runtime-core/src/component.ts
。
setupStatefulComponent
接下來筆者就帶着大家一起來剖析一下 setupStatefulComponent 的過程:
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
if (__DEV__) { /* 檢測組件名稱、指令、編譯選項等等,有錯誤則報警 */ }
// 0. 創建一個渲染代理的屬性的訪問緩存
instance.accessCache = Object.create(null)
// 1. 創建一個公共的示例或渲染器代理
// 它將被標記爲 raw,所以它不會被追蹤
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 2. 調用 setup()
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
currentInstance = null
if (isPromise(setupResult)) {
if (isSSR) {
// 返回一個 promise,因此服務端渲染可以等待它執行。
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
}
} else {
// 捕獲 Setup 執行結果
handleSetupResult(instance, setupResult, isSSR)
}
} else {
// 完成組件初始化
finishComponentSetup(instance, isSSR)
}
}
組件一開始會初始化一個 Component 變量,其中保存着組件的選項。接下來如果是 DEV 環境,則會開始檢測組件中的各種選項的命名,比如 name、components、directives 等,如果檢測有問題,就會在開發環境報出警告。
在檢測完畢後,會開始正經的初始化過程,首先會在實例上創建一個 accessCache 的屬性,該屬性用以緩存渲染器代理屬性,以減少讀取次數。之後會在組件實例上初始化一個代理屬性,這個代理屬性代理了組件的上下文,並且將它設置爲觀察原始值,這樣這個代理對象將不會被追蹤。
之後就開始處理我們本文關心的 setup 邏輯了。首先從組件中取出 setup 函數,這裏判斷是否存在 setup 函數,如果不存在,則直接跳轉到底部邏輯,執行 finishComponentSetup,完成組件初始化。否則就會進入 if (setup)
之後的分支條件中。
是否執行 createSetupContext 生成 setup 的上下文對象,取決於 setup 函數中形參的數量是否大於 1。
這裏需要注意的一個知識點是:在 function 函數對象上調用 length 時,返回值是這個函數的形參數量。
舉個例子:
setup() // setup.length === 0
setup(props) // setup.length === 1
setup(props, { emit, attrs }) // setup.length === 2
默認情況下,props 是調用 setup 時必傳的參數,所以是否需要去生成 setup 的上下文的條件就是 setup.length > 1 。
那麼順着代碼邏輯,我們一起來看一下 setup 上下文中究竟有些什麼東西。
export function createSetupContext(
instance: ComponentInternalInstance
): SetupContext {
const expose: SetupContext['expose'] = exposed => {
instance.exposed = proxyRefs(exposed)
}
if (__DEV__) {
/* DEV 邏輯忽略,對上下文選項設置 getter */
} else {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose
}
}
}
expose 的妙用
看到這段 createSetupContext 函數的邏輯,我們發現 setup 上下文中就如文檔中描述的一樣,有 attrs、slots、emit 這三種熟悉的屬性,而在這裏驚奇的發現竟然還有一個文檔中未說明的 expose 屬性返回。
expose 是早先 Vue RFC 中的一個提案,expose 的設想是提供一個像 expose({ ...publicMembers })
這樣的組合式 API,這樣組件的作者就可以在 setup() 中使用該 API 來清除地控制哪些內容會明確地公開暴露給組件使用者。
當你在封裝組件時,如果嫌 ref 中暴露的內容過多,不妨用 expose 來約束一下輸出。當然這還僅僅是一個 RFC 提案,感興趣的小夥伴可以偷偷嚐鮮哦。
import { ref } from 'vue'
export default {
setup(_, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
// 僅僅暴露 increment 給父組件
expose({
increment
})
return { increment, count }
}
}
例如當你像上方代碼一樣使用 expose 時,父組件獲取的 ref 對象裏只會有 increment 屬性,而 count 屬性將不會暴露出去。
執行 setup 函數
在處理完 setupContext 的上下文後,組件會停止依賴收集,並且開始執行 setup 函數。
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
Vue 會通過 callWithErrorHandling 調用 setup 函數,這裏我們可以看最後一行,是作爲 args 參數傳入的,與上文描述一樣,props 會始終傳入,若是 setup.length <= 1 , setupContext 則爲 null。
調用完 setup 之後,會重置依賴收集狀態。接下來判斷 setupResult 的返回值類型。
如果 setup 函數的返回值是 promise 類型,並且是服務端渲染的,則會等待繼續執行。否則就會報錯,說當前版本的 Vue 並不支持 setup 返回 promise 對象。
如果不是 promise 類型返回值,則會通過 handleSetupResult 函數來處理返回結果。
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup 返回了一個行內渲染函數
if (__NODE_JS__ && (instance.type as ComponentOptions).__ssrInlineRender) {
// 當這個函數的名字是 ssrRender (通過 SFC 的行內模式編譯)
// 將函數作爲服務端渲染函數
instance.ssrRender = setupResult
} else {
// 否則將函數作爲渲染函數
instance.render = setupResult as InternalRenderFunction
}
} else if (isObject(setupResult)) {
// 將返回對象轉換爲響應式對象,並設置爲實例的 setupState 屬性
instance.setupState = proxyRefs(setupResult)
}
finishComponentSetup(instance, isSSR)
}
在 handleSetupResult 這個結果捕獲函數中,首先判斷 setup 返回結果的類型,如果是一個函數,並且又是服務端的行內模式渲染函數,則將該結果作爲 ssrRender 屬性;而在非服務端渲染的情況下,會直接當做 render 函數來處理。
接着會判斷 setup 返回結果如果是對象,就會將這個對象轉換成一個代理對象,並設置爲組件實例的 setupState 屬性。
最終還是會跟其他沒有 setup 函數的組件一樣,調用 finishComponentSetup 完成組件的創建。
finishComponentSetup
這個函數的主要作用是獲取併爲組件設置渲染函數,對於模板(template)以及渲染函數的獲取方式有以下三種規範行爲:
1、渲染函數可能已經存在,通過 setup 返回了結果。例如我們在上一節講的 setup 的返回值爲函數的情況。
2、如果 setup 沒有返回,則嘗試獲取組件模板並編譯,從 Component.render
中獲取渲染函數,
3、如果這個函數還是沒有渲染函數,則將 instance.render
設置爲空,以便它能從 mixins/extend 等方式中獲取渲染函數。
這個在這種規範行爲的指導下,首先判斷了服務端渲染的情況,接着判斷沒有 instance.render 存在的情況,當進行這種判斷時已經說明組件並沒有從 setup 中獲得渲染函數,在進行第二種行爲的嘗試。從組件中獲取模板,設置好編譯選項後調用 Component.render = compile(template, finalCompilerOptions)
進行編譯,這部分編譯的知識在我的第一篇文章編譯流程中有過詳細介紹。
最後將編譯後的渲染函數賦值給組件實例的 render 屬性,如果沒有則賦值爲 NOOP 空函數。
接着判斷渲染函數是否是使用了 with 塊包裹的運行時編譯的渲染函數,如果是這種情況則會將渲染代理設置爲一個不同的 has
handler 代理陷阱,它的性能更強並且能夠去避免檢測一些全局變量。
至此組件的初始化完畢,渲染函數也設置結束了。
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
// 模板 / 渲染函數的規範行爲
// 1、渲染函數可能已經存在,通過 setup 返回
// 2、除此之外嘗試使用 `Component.render` 當做渲染函數
// 3、如果這個函數沒有渲染函數,設置 `instance.render` 爲空函數,以便它能從 mixins/extend 中獲得渲染函數
if (__NODE_JS__ && isSSR) {
instance.render = (instance.render ||
Component.render ||
NOOP) as InternalRenderFunction
} else if (!instance.render) {
// 可以在 setup() 中設置
if (compile && !Component.render) {
const template = Component.template
if (template) {
const { isCustomElement, compilerOptions } = instance.appContext.config
const {
delimiters,
compilerOptions: componentCompilerOptions
} = Component
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)
Component.render = compile(template, finalCompilerOptions)
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
// 對於使用 `with` 塊的運行時編譯的渲染函數,這個渲染代理需要不一樣的 `has` handler 陷阱,它有更好的
// 性能表現並且只允許白名單內的 globals 屬性通過。
if (instance.render._rc) {
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
)
}
}
}
總結
今天筆者介紹了一個有狀態的組件的初始化的過程,在 setup 函數初始化部分進行了仔細的講解,我們不僅學習了 setup 上下文初始化的條件,也明確的知曉了 setup 上下文究竟給我們暴露了哪些屬性,並且從中學到了一個新的 RFC 提案: expose 屬性。
我們學習了 setup 函數執行的過程以及 Vue 是如何處理捕獲 setup 的返回結果的。
最後我們講解了組件初始化時,不論是否使用 setup 都會執行的 finishComponentSetup 函數,通過這個函數內部的邏輯我們瞭解了一個組件在初始化完畢時,渲染函數設置的規則。
最後,如果這篇文章能夠幫助到你瞭解 Vue3 中 setup 的小細節,希望能給本文點一個喜歡❤️。如果想繼續追蹤後續文章,也可以關注我的賬號或 follow 我的 github,再次謝謝各位可愛的看官老爺。