vue3 setup的使用和原理實(shí)例詳解
1.前言
為什么要使用setup?
寫一個(gè)大型組件時(shí),邏輯關(guān)注點(diǎn)的列表很長(zhǎng),不利于維護(hù)和閱讀;所以需要把一個(gè)邏輯關(guān)注點(diǎn)的代碼收集在一起會(huì)更好,由此誕生組合式API,即vue中用到的setup。
就像我們要做鹵雞蛋,原來(lái)材料有四堆香料:八角、桂皮、香葉、茴香(data、method、computed、watch);一樣一樣找太麻煩,就打包做成香料袋,一次放一個(gè)袋子(setup)就行
最近在做vue3相關(guān)的項(xiàng)目,用到了組合式api,對(duì)于vue3的語(yǔ)法的改進(jìn)也是大為贊賞,用起來(lái)十分方便。對(duì)于已經(jīng)熟悉vue2寫法的同學(xué)也說(shuō),上手還是需要一定的學(xué)習(xí)成本,有可能目前停留在會(huì)寫會(huì)用的階段,但是setup帶來(lái)哪些改變,以及ref,reactive這兩api內(nèi)部實(shí)現(xiàn)原理到底是什么,下面先來(lái)總結(jié):
setup帶來(lái)的改變:
1.解決了vue2的data和methods方法相距太遠(yuǎn),無(wú)法組件之間復(fù)用
2.提供了script標(biāo)簽引入共同業(yè)務(wù)邏輯的代碼塊,順序執(zhí)行
3.script變成setup函數(shù),默認(rèn)暴露給模版
4.組件直接掛載,無(wú)需注冊(cè)
5.自定義的指令也可以在模版中自動(dòng)獲得
6.this不再是這個(gè)活躍實(shí)例的引用
7.帶來(lái)的大量全新api,比如defineProps,defineEmits,withDefault,toRef,toRefs
ref帶來(lái)的改變:
Vue 提供了一個(gè) ref() 方法來(lái)允許我們創(chuàng)建可以使用任何值類型的響應(yīng)式數(shù)據(jù)
Ref作TS的類型標(biāo)注
reactive帶來(lái)的改變:
可以使用 reactive() 函數(shù)創(chuàng)建一個(gè)響應(yīng)式對(duì)象或數(shù)組
reactive可以隱式地從它的參數(shù)中推導(dǎo)類型
使用interface進(jìn)行類型標(biāo)注
需要了解vue2和vue3區(qū)別的可以查看我的這篇文章:
2.setup
在 setup()
函數(shù)中手動(dòng)暴露大量的狀態(tài)和方法非常繁瑣。幸運(yùn)的是,我們可以通過(guò)使用構(gòu)建工具來(lái)簡(jiǎn)化該操作。當(dāng)使用單文件組件(SFC)時(shí),我們可以使用 <script setup>
來(lái)大幅度地簡(jiǎn)化代碼。
<script setup>
中的頂層的導(dǎo)入和變量聲明可在同一組件的模板中直接使用。你可以理解為模板中的表達(dá)式和 <script setup>
中的代碼處在同一個(gè)作用域中。
里面的代碼會(huì)被編譯成組件 setup() 函數(shù)的內(nèi)容
。這意味著與普通的 <script>
只在組件被首次引入
的時(shí)候執(zhí)行一次
不同,<script setup>
中的代碼會(huì)在每次
組件實(shí)例被創(chuàng)建
的時(shí)候執(zhí)行。
官方解答:
<script setup>
是在單文件組件 (SFC) 中使用組合式 API 的編譯時(shí)語(yǔ)法糖。當(dāng)同時(shí)使用 SFC 與組合式 API 時(shí)該語(yǔ)法是默認(rèn)推薦。相比于普通的<script>
語(yǔ)法,它具有更多優(yōu)勢(shì):
- 更少的樣板內(nèi)容,更簡(jiǎn)潔的代碼。
- 能夠使用純 TypeScript 聲明 props 和自定義事件。
- 更好的運(yùn)行時(shí)性能 (其模板會(huì)被編譯成同一作用域內(nèi)的渲染函數(shù),避免了渲染上下文代理對(duì)象)。
- 更好的 IDE 類型推導(dǎo)性能 (減少了語(yǔ)言服務(wù)器從代碼中抽取類型的工作)。
setup執(zhí)行是在創(chuàng)建實(shí)例之前就是beforeCreate執(zhí)行,所以setup函數(shù)中的this還不是組件的實(shí)例,而是undefined,setup是同步的。
setup?: (this: void, props: Readonly<LooseRequired<Props & UnionToIntersection<ExtractOptionProp<Mixin>> & UnionToIntersection<ExtractOptionProp<Extends>>>>, ctx: SetupContext<E>) => Promise<RawBindings> | RawBindings | RenderFunction | void;)
在上面的代碼中我們了解到了第一個(gè)參數(shù)props,還有第二個(gè)參數(shù)context。
props是接受父組件傳遞過(guò)來(lái)的所有的屬性和方法;context是一個(gè)對(duì)象,這個(gè)對(duì)象不是響應(yīng)式的,可以進(jìn)行解構(gòu)賦值。存在屬性為attrs:instance.slots,slots: instance.slots,emit: instance.emit。
setup(props, { attrs, slots, emit, expose }) { ? ?... ?} ?或 ?setup(props, content) { ? ?const { attrs, slots, emit, expose } = content ?}
這里要注意一下,attrs 和 slots 是有狀態(tài)的對(duì)象,它們總是會(huì)隨組件本身的更新而更新。這意味著你應(yīng)該避免對(duì)它們進(jìn)行解構(gòu),并始終以 attrs.x 或 slots.x 的方式引用 property。請(qǐng)注意,與 props 不同,attrs 和 slots 的 property 是非響應(yīng)式的。如果你打算根據(jù) attrs 或 slots 的更改應(yīng)用副作用,那么應(yīng)該在 onBeforeUpdate 生命周期鉤子中執(zhí)行此操作。
3.源碼分析
在vue的3.2.3x版本中,處理setup函數(shù)源碼文件位于:node_moudles/@vue/runtime-core/dist/runtime-core.cjs.js文件中。
setupStatefulComponent
下面開始解析一下setupStatefulComponent的執(zhí)行過(guò)程:
function setupStatefulComponent(instance, isSSR) { var _a; const Component = instance.type; { if (Component.name) { validateComponentName(Component.name, instance.appContext.config); } if (Component.components) { const names = Object.keys(Component.components); for (let i = 0; i < names.length; i++) { validateComponentName(names[i], instance.appContext.config); } } if (Component.directives) { const names = Object.keys(Component.directives); for (let i = 0; i < names.length; i++) { validateDirectiveName(names[i]); } } if (Component.compilerOptions && isRuntimeOnly()) { warn(`"compilerOptions" is only supported when using a build of Vue that ` + `includes the runtime compiler. Since you are using a runtime-only ` + `build, the options should be passed via your build tool config instead.`); } } // 0. create render proxy property access cache instance.accessCache = Object.create(null); // 1. create public instance / render proxy // also mark it raw so it's never observed instance.proxy = reactivity.markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)); { exposePropsOnRenderContext(instance); } // 2. call setup() const { setup } = Component; if (setup) { const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null); setCurrentInstance(instance); reactivity.pauseTracking(); const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [reactivity.shallowReadonly(instance.props) , setupContext]); reactivity.resetTracking(); unsetCurrentInstance(); if (shared.isPromise(setupResult)) { setupResult.then(unsetCurrentInstance, unsetCurrentInstance); if (isSSR) { // return the promise so server-renderer can wait on it return setupResult .then((resolvedResult) => { handleSetupResult(instance, resolvedResult, isSSR); }) .catch(e => { handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */); }); } else { // async setup returned Promise. // bail here and wait for re-entry. instance.asyncDep = setupResult; if (!instance.suspense) { const name = (_a = Component.name) !== null && _a !== void 0 ? _a : 'Anonymous'; warn(`Component <${name}>: setup function returned a promise, but no ` + `<Suspense> boundary was found in the parent component tree. ` + `A component with async setup() must be nested in a <Suspense> ` + `in order to be rendered.`); } } } else { handleSetupResult(instance, setupResult, isSSR); } } else { finishComponentSetup(instance, isSSR); } }
函數(shù)接受兩個(gè)參數(shù),一個(gè)是組建實(shí)例,另一個(gè)是是否ssr渲染,接下來(lái)是驗(yàn)證過(guò)程,這里的文件是開發(fā)環(huán)境文件, DEV 環(huán)境,則會(huì)開始檢測(cè)組件中的各種選項(xiàng)的命名,比如 name、components、directives 等,如果檢測(cè)有問(wèn)題,就會(huì)在開發(fā)環(huán)境報(bào)出警告。
檢測(cè)完成之后,進(jìn)行初始化,生成一個(gè)accessCached的屬性對(duì)象,該屬性用以緩存渲染器代理屬性,以減少讀取次數(shù)。然后在初始化一個(gè)代理的屬性,instance.proxy = reactivity.markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));這個(gè)代理屬性代理了組件的上下文,并且將它設(shè)置為觀察原始值,這樣這個(gè)代理對(duì)象將不會(huì)被追蹤。
接下來(lái)便是setup的核心邏輯了,如果組件上有setup 函數(shù),繼續(xù)執(zhí)行,如果不存在跳到尾部,執(zhí)行finishComponentSetup(instance, isSSR),完成組件的初始化,否則就會(huì)進(jìn)入 if (setup)
之后的分支條件中。是否執(zhí)行setup生成上下文取決于setup.length > 1 ?createSetupContext(instance) : null。
來(lái)看一下setup執(zhí)行上下文究竟有哪些東西:
function createSetupContext(instance) { const expose = exposed => { if (instance.exposed) { warn(`expose() should be called only once per setup().`); } instance.exposed = exposed || {}; }; let attrs; { // We use getters in dev in case libs like test-utils overwrite instance // properties (overwrites should not be done in prod) return Object.freeze({ get attrs() { return attrs || (attrs = createAttrsProxy(instance)); }, get slots() { return reactivity.shallowReadonly(instance.slots); }, get emit() { return (event, ...args) => instance.emit(event, ...args); }, expose }); } }
expose解析:
可以在 setup() 中使用該 API 來(lái)清除地控制哪些內(nèi)容會(huì)明確地公開暴露給組件使用者。
當(dāng)你在封裝組件時(shí),如果嫌 ref 中暴露的內(nèi)容過(guò)多,不妨用 expose 來(lái)約束一下輸出。
import { ref } from 'vue' export default { setup(_, { expose }) { const count = ref(0) function increment() { count.value++ } // 僅僅暴露 increment 給父組件 expose({ increment }) return { increment, count } } }
例如當(dāng)你像上方代碼一樣使用 expose 時(shí),父組件獲取的 ref 對(duì)象里只會(huì)有 increment 屬性,而 count 屬性將不會(huì)暴露出去。
執(zhí)行setup函數(shù)
在處理完 createSetupContext 的上下文后,組件會(huì)停止依賴收集,并且開始執(zhí)行 setup 函數(shù)。
const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [reactivity.shallowReadonly(instance.props) , setupContext]);
Vue 會(huì)通過(guò) callWithErrorHandling 調(diào)用 setup 函數(shù),組件實(shí)例instance傳入,這里我們可以看最后一行,是作為 args 參數(shù)傳入的,與上文描述一樣,props 會(huì)始終傳入,若是 setup.length <= 1 , setupContext 則為 null。
調(diào)用玩setup之后,會(huì)重置收集的狀態(tài),reactivity.resetTracking(),接下來(lái)是判斷setupResult的類型。
if (shared.isPromise(setupResult)) { setupResult.then(unsetCurrentInstance, unsetCurrentInstance); if (isSSR) { // return the promise so server-renderer can wait on it return setupResult .then((resolvedResult) => { handleSetupResult(instance, resolvedResult, isSSR); }) .catch(e => { handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */); }); } else { // async setup returned Promise. // bail here and wait for re-entry. instance.asyncDep = setupResult; if (!instance.suspense) { const name = (_a = Component.name) !== null && _a !== void 0 ? _a : 'Anonymous'; warn(`Component <${name}>: setup function returned a promise, but no ` + `<Suspense> boundary was found in the parent component tree. ` + `A component with async setup() must be nested in a <Suspense> ` + `in order to be rendered.`); } } }
如果 setup 函數(shù)的返回值是 promise 類型,并且是服務(wù)端渲染的,則會(huì)等待繼續(xù)執(zhí)行。否則就會(huì)報(bào)錯(cuò),說(shuō)當(dāng)前版本的 Vue 并不支持 setup 返回 promise 對(duì)象。
如果不是 promise 類型返回值,則會(huì)通過(guò) handleSetupResult 函數(shù)來(lái)處理返回結(jié)果。
else { handleSetupResult(instance, setupResult, isSSR); }
function handleSetupResult(instance, setupResult, isSSR) { if (shared.isFunction(setupResult)) { // setup returned an inline render function if (instance.type.__ssrInlineRender) { // when the function's name is `ssrRender` (compiled by SFC inline mode), // set it as ssrRender instead. instance.ssrRender = setupResult; } else { instance.render = setupResult; } } else if (shared.isObject(setupResult)) { if (isVNode(setupResult)) { warn(`setup() should not return VNodes directly - ` + `return a render function instead.`); } // setup returned bindings. // assuming a render function compiled from template is present. { instance.devtoolsRawSetupState = setupResult; } instance.setupState = reactivity.proxyRefs(setupResult); { exposeSetupStateOnRenderContext(instance); } } else if (setupResult !== undefined) { warn(`setup() should return an object. Received: ${setupResult === null ? 'null' : typeof setupResult}`); } finishComponentSetup(instance, isSSR); }
在 handleSetupResult 這個(gè)結(jié)果捕獲函數(shù)中,首先判斷 setup 返回結(jié)果的類型,如果是一個(gè)函數(shù),并且又是服務(wù)端的行內(nèi)模式渲染函數(shù),則將該結(jié)果作為 ssrRender 屬性;而在非服務(wù)端渲染的情況下,會(huì)直接當(dāng)做 render 函數(shù)來(lái)處理。
接著會(huì)判斷 setup 返回結(jié)果如果是對(duì)象,就會(huì)將這個(gè)對(duì)象轉(zhuǎn)換成一個(gè)代理對(duì)象,并設(shè)置為組件實(shí)例的 setupState 屬性。
最終還是會(huì)跟其他沒(méi)有 setup 函數(shù)的組件一樣,調(diào)用 finishComponentSetup 完成組件的創(chuàng)建。
finishComponentSetup
function finishComponentSetup(instance, isSSR, skipOptions) { const Component = instance.type; // template / render function normalization // could be already set when returned from setup() if (!instance.render) { // only do on-the-fly compile if not in SSR - SSR on-the-fly compilation // is done by server-renderer if (!isSSR && compile && !Component.render) { const template = Component.template; if (template) { { startMeasure(instance, `compile`); } const { isCustomElement, compilerOptions } = instance.appContext.config; const { delimiters, compilerOptions: componentCompilerOptions } = Component; const finalCompilerOptions = shared.extend(shared.extend({ isCustomElement, delimiters }, compilerOptions), componentCompilerOptions); Component.render = compile(template, finalCompilerOptions); { endMeasure(instance, `compile`); } } } instance.render = (Component.render || shared.NOOP); // for runtime-compiled render functions using `with` blocks, the render // proxy used needs a different `has` handler which is more performant and // also only allows a whitelist of globals to fallthrough. if (installWithProxy) { installWithProxy(instance); } } // support for 2.x options { setCurrentInstance(instance); reactivity.pauseTracking(); applyOptions(instance); reactivity.resetTracking(); unsetCurrentInstance(); } // warn missing template/render // the runtime compilation of template in SSR is done by server-render if (!Component.render && instance.render === shared.NOOP && !isSSR) { /* istanbul ignore if */ if (!compile && Component.template) { warn(`Component provided template option but ` + `runtime compilation is not supported in this build of Vue.` + (``) /* should not happen */); } else { warn(`Component is missing template or render function.`); } } }
這個(gè)函數(shù)的主要作用是獲取并為組件設(shè)置渲染函數(shù),對(duì)于模板(template)以及渲染函數(shù)的獲取方式有以下三種規(guī)范行為:
1、渲染函數(shù)可能已經(jīng)存在,通過(guò) setup 返回了結(jié)果。例如我們?cè)谏弦还?jié)講的 setup 的返回值為函數(shù)的情況。
2、如果 setup 沒(méi)有返回,則嘗試獲取組件模板并編譯,從 Component.render
中獲取渲染函數(shù),
3、如果這個(gè)函數(shù)還是沒(méi)有渲染函數(shù),則將 instance.render
設(shè)置為空,以便它能從 mixins/extend 等方式中獲取渲染函數(shù)。
這個(gè)在這種規(guī)范行為的指導(dǎo)下,首先判斷了服務(wù)端渲染的情況,接著判斷沒(méi)有 instance.render 存在的情況,當(dāng)進(jìn)行這種判斷時(shí)已經(jīng)說(shuō)明組件并沒(méi)有從 setup 中獲得渲染函數(shù),在進(jìn)行第二種行為的嘗試。從組件中獲取模板,設(shè)置好編譯選項(xiàng)后調(diào)用Component.render = compile(template, finalCompilerOptions);進(jìn)行編譯,編譯過(guò)程不再贅述。
最后將編譯后的渲染函數(shù)賦值給組件實(shí)例的 render 屬性,如果沒(méi)有則賦值為 NOOP 空函數(shù)。
接著判斷渲染函數(shù)是否是使用了 with 塊包裹的運(yùn)行時(shí)編譯的渲染函數(shù),如果是這種情況則會(huì)將渲染代理設(shè)置為一個(gè)不同的 has
handler 代理陷阱,它的性能更強(qiáng)并且能夠去避免檢測(cè)一些全局變量。
至此組件的初始化完畢,渲染函數(shù)也設(shè)置結(jié)束了。
4.總結(jié)
在vue3中,新的setup函數(shù)屬性給我們提供了書寫的便利,其背后的工作量無(wú)疑是巨大的,有狀態(tài)的組件的初始化的過(guò)程,在 setup 函數(shù)初始化部分我們討論的源碼的執(zhí)行過(guò)程,我們不僅學(xué)習(xí)了 setup 上下文初始化的條件,也明確的知曉了 setup 上下文究竟給我們暴露了哪些屬性,并且從中學(xué)到了一個(gè)新的 RFC 提案屬性: expose 屬性
我們學(xué)習(xí)了 setup 函數(shù)執(zhí)行的過(guò)程以及 Vue 是如何處理捕獲 setup 的返回結(jié)果的。
然后我們講解了組件初始化時(shí),不論是否使用 setup 都會(huì)執(zhí)行的 finishComponentSetup 函數(shù),通過(guò)這個(gè)函數(shù)內(nèi)部的邏輯我們了解了一個(gè)組件在初始化完畢時(shí),渲染函數(shù)設(shè)置的規(guī)則。
- vue3中setup語(yǔ)法糖下父子組件間傳遞數(shù)據(jù)的方式
- Vue3.2中setup語(yǔ)法糖的使用教程分享
- vue3中<script?setup>?和?setup函數(shù)的區(qū)別對(duì)比
- Vue3?setup的注意點(diǎn)及watch監(jiān)視屬性的六種情況分析
- vue3在setup中使用mapState解讀
- Vue3中關(guān)于setup與自定義指令詳解
- Vue3中的setup語(yǔ)法糖、computed函數(shù)、watch函數(shù)詳解
- Vue3?setup?的作用實(shí)例詳解
- Vue3?setup添加name的方法步驟
- Vue3的setup在el-tab中動(dòng)態(tài)加載組件的方法
- vue3.0?setup中使用vue-router問(wèn)題
- vue3中setup語(yǔ)法糖下通用的分頁(yè)插件實(shí)例詳解
- vue3?setup語(yǔ)法糖各種語(yǔ)法新特性的使用方法(vue3+vite+pinia)
相關(guān)文章
vue配置文件自動(dòng)生成路由和菜單實(shí)例代碼
因?yàn)椴煌挠脩粲胁煌臋?quán)限,能訪問(wèn)的頁(yè)面是不一樣的,所以我們?cè)趯懞笈_(tái)管理系統(tǒng)時(shí)就會(huì)遇過(guò)這樣的需求:根據(jù)后臺(tái)數(shù)據(jù)動(dòng)態(tài)添加路由和菜單,這篇文章主要給大家介紹了關(guān)于vue配置文件自動(dòng)生成路由和菜單的相關(guān)資料,需要的朋友可以參考下2021-08-08vue報(bào)錯(cuò)Error:Cannot?find?module?'fs/promises'的解決方
最近的項(xiàng)目需要用到vue/cli,但是用cnpm安裝vue/cli的時(shí)候報(bào)錯(cuò)了,下面這篇文章主要給大家介紹了關(guān)于vue報(bào)錯(cuò)Error:Cannot?find?module?'fs/promises'的解決方式,需要的朋友可以參考下2022-11-11使用vue初用antd 用v-model來(lái)雙向綁定Form表單問(wèn)題
這篇文章主要介紹了使用vue初用antd 用v-model來(lái)雙向綁定Form表單問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04vue第三方庫(kù)中存在擴(kuò)展運(yùn)算符報(bào)錯(cuò)問(wèn)題的解決方案
這篇文章主要介紹了vue第三方庫(kù)中存在擴(kuò)展運(yùn)算符報(bào)錯(cuò)問(wèn)題,本文給大家分享解決方案,通過(guò)結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07使用vue打包進(jìn)行云服務(wù)器上傳的問(wèn)題
這篇文章主要介紹了使用vue打包進(jìn)行云服務(wù)器上傳,本文給大家介紹的非常詳細(xì),對(duì)大家的工作或?qū)W習(xí)具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03Vue項(xiàng)目如何實(shí)現(xiàn)切換主題色思路
這篇文章主要介紹了Vue項(xiàng)目如何實(shí)現(xiàn)切換主題色思路,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01vue2.0實(shí)現(xiàn)移動(dòng)端的輸入框?qū)崟r(shí)檢索更新列表功能
最近小編在做vue2.0的項(xiàng)目,遇到移動(dòng)端實(shí)時(shí)檢索搜索更新列表的效果,下面腳本之家小編給大家?guī)?lái)了vue2.0 移動(dòng)端的輸入框?qū)崟r(shí)檢索更新列表功能的實(shí)例代碼,感興趣的朋友參考下吧2018-05-05