vue3 setup的使用和原理實例詳解
1.前言
為什么要使用setup?
寫一個大型組件時,邏輯關注點的列表很長,不利于維護和閱讀;所以需要把一個邏輯關注點的代碼收集在一起會更好,由此誕生組合式API,即vue中用到的setup。
就像我們要做鹵雞蛋,原來材料有四堆香料:八角、桂皮、香葉、茴香(data、method、computed、watch);一樣一樣找太麻煩,就打包做成香料袋,一次放一個袋子(setup)就行
最近在做vue3相關的項目,用到了組合式api,對于vue3的語法的改進也是大為贊賞,用起來十分方便。對于已經熟悉vue2寫法的同學也說,上手還是需要一定的學習成本,有可能目前停留在會寫會用的階段,但是setup帶來哪些改變,以及ref,reactive這兩api內部實現(xiàn)原理到底是什么,下面先來總結:
setup帶來的改變:
1.解決了vue2的data和methods方法相距太遠,無法組件之間復用
2.提供了script標簽引入共同業(yè)務邏輯的代碼塊,順序執(zhí)行
3.script變成setup函數(shù),默認暴露給模版
4.組件直接掛載,無需注冊
5.自定義的指令也可以在模版中自動獲得
6.this不再是這個活躍實例的引用
7.帶來的大量全新api,比如defineProps,defineEmits,withDefault,toRef,toRefs
ref帶來的改變:
Vue 提供了一個 ref() 方法來允許我們創(chuàng)建可以使用任何值類型的響應式數(shù)據(jù)
Ref作TS的類型標注
reactive帶來的改變:
可以使用 reactive() 函數(shù)創(chuàng)建一個響應式對象或數(shù)組
reactive可以隱式地從它的參數(shù)中推導類型
使用interface進行類型標注
需要了解vue2和vue3區(qū)別的可以查看我的這篇文章:
2.setup
在 setup()
函數(shù)中手動暴露大量的狀態(tài)和方法非常繁瑣。幸運的是,我們可以通過使用構建工具來簡化該操作。當使用單文件組件(SFC)時,我們可以使用 <script setup>
來大幅度地簡化代碼。
<script setup>
中的頂層的導入和變量聲明可在同一組件的模板中直接使用。你可以理解為模板中的表達式和 <script setup>
中的代碼處在同一個作用域中。
里面的代碼會被編譯成組件 setup() 函數(shù)的內容
。這意味著與普通的 <script>
只在組件被首次引入
的時候執(zhí)行一次
不同,<script setup>
中的代碼會在每次
組件實例被創(chuàng)建
的時候執(zhí)行。
官方解答:
<script setup>
是在單文件組件 (SFC) 中使用組合式 API 的編譯時語法糖。當同時使用 SFC 與組合式 API 時該語法是默認推薦。相比于普通的<script>
語法,它具有更多優(yōu)勢:
- 更少的樣板內容,更簡潔的代碼。
- 能夠使用純 TypeScript 聲明 props 和自定義事件。
- 更好的運行時性能 (其模板會被編譯成同一作用域內的渲染函數(shù),避免了渲染上下文代理對象)。
- 更好的 IDE 類型推導性能 (減少了語言服務器從代碼中抽取類型的工作)。
setup執(zhí)行是在創(chuàng)建實例之前就是beforeCreate執(zhí)行,所以setup函數(shù)中的this還不是組件的實例,而是undefined,setup是同步的。
setup?: (this: void, props: Readonly<LooseRequired<Props & UnionToIntersection<ExtractOptionProp<Mixin>> & UnionToIntersection<ExtractOptionProp<Extends>>>>, ctx: SetupContext<E>) => Promise<RawBindings> | RawBindings | RenderFunction | void;)
在上面的代碼中我們了解到了第一個參數(shù)props,還有第二個參數(shù)context。
props是接受父組件傳遞過來的所有的屬性和方法;context是一個對象,這個對象不是響應式的,可以進行解構賦值。存在屬性為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)的對象,它們總是會隨組件本身的更新而更新。這意味著你應該避免對它們進行解構,并始終以 attrs.x 或 slots.x 的方式引用 property。請注意,與 props 不同,attrs 和 slots 的 property 是非響應式的。如果你打算根據(jù) attrs 或 slots 的更改應用副作用,那么應該在 onBeforeUpdate 生命周期鉤子中執(zhí)行此操作。
3.源碼分析
在vue的3.2.3x版本中,處理setup函數(shù)源碼文件位于:node_moudles/@vue/runtime-core/dist/runtime-core.cjs.js文件中。
setupStatefulComponent
下面開始解析一下setupStatefulComponent的執(zhí)行過程:
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ù)接受兩個參數(shù),一個是組建實例,另一個是是否ssr渲染,接下來是驗證過程,這里的文件是開發(fā)環(huán)境文件, DEV 環(huán)境,則會開始檢測組件中的各種選項的命名,比如 name、components、directives 等,如果檢測有問題,就會在開發(fā)環(huán)境報出警告。
檢測完成之后,進行初始化,生成一個accessCached的屬性對象,該屬性用以緩存渲染器代理屬性,以減少讀取次數(shù)。然后在初始化一個代理的屬性,instance.proxy = reactivity.markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));這個代理屬性代理了組件的上下文,并且將它設置為觀察原始值,這樣這個代理對象將不會被追蹤。
接下來便是setup的核心邏輯了,如果組件上有setup 函數(shù),繼續(xù)執(zhí)行,如果不存在跳到尾部,執(zhí)行finishComponentSetup(instance, isSSR),完成組件的初始化,否則就會進入 if (setup)
之后的分支條件中。是否執(zhí)行setup生成上下文取決于setup.length > 1 ?createSetupContext(instance) : null。
來看一下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 來清除地控制哪些內容會明確地公開暴露給組件使用者。
當你在封裝組件時,如果嫌 ref 中暴露的內容過多,不妨用 expose 來約束一下輸出。
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 屬性將不會暴露出去。
執(zhí)行setup函數(shù)
在處理完 createSetupContext 的上下文后,組件會停止依賴收集,并且開始執(zhí)行 setup 函數(shù)。
const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [reactivity.shallowReadonly(instance.props) , setupContext]);
Vue 會通過 callWithErrorHandling 調用 setup 函數(shù),組件實例instance傳入,這里我們可以看最后一行,是作為 args 參數(shù)傳入的,與上文描述一樣,props 會始終傳入,若是 setup.length <= 1 , setupContext 則為 null。
調用玩setup之后,會重置收集的狀態(tài),reactivity.resetTracking(),接下來是判斷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 類型,并且是服務端渲染的,則會等待繼續(xù)執(zhí)行。否則就會報錯,說當前版本的 Vue 并不支持 setup 返回 promise 對象。
如果不是 promise 類型返回值,則會通過 handleSetupResult 函數(shù)來處理返回結果。
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 這個結果捕獲函數(shù)中,首先判斷 setup 返回結果的類型,如果是一個函數(shù),并且又是服務端的行內模式渲染函數(shù),則將該結果作為 ssrRender 屬性;而在非服務端渲染的情況下,會直接當做 render 函數(shù)來處理。
接著會判斷 setup 返回結果如果是對象,就會將這個對象轉換成一個代理對象,并設置為組件實例的 setupState 屬性。
最終還是會跟其他沒有 setup 函數(shù)的組件一樣,調用 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.`); } } }
這個函數(shù)的主要作用是獲取并為組件設置渲染函數(shù),對于模板(template)以及渲染函數(shù)的獲取方式有以下三種規(guī)范行為:
1、渲染函數(shù)可能已經存在,通過 setup 返回了結果。例如我們在上一節(jié)講的 setup 的返回值為函數(shù)的情況。
2、如果 setup 沒有返回,則嘗試獲取組件模板并編譯,從 Component.render
中獲取渲染函數(shù),
3、如果這個函數(shù)還是沒有渲染函數(shù),則將 instance.render
設置為空,以便它能從 mixins/extend 等方式中獲取渲染函數(shù)。
這個在這種規(guī)范行為的指導下,首先判斷了服務端渲染的情況,接著判斷沒有 instance.render 存在的情況,當進行這種判斷時已經說明組件并沒有從 setup 中獲得渲染函數(shù),在進行第二種行為的嘗試。從組件中獲取模板,設置好編譯選項后調用Component.render = compile(template, finalCompilerOptions);進行編譯,編譯過程不再贅述。
最后將編譯后的渲染函數(shù)賦值給組件實例的 render 屬性,如果沒有則賦值為 NOOP 空函數(shù)。
接著判斷渲染函數(shù)是否是使用了 with 塊包裹的運行時編譯的渲染函數(shù),如果是這種情況則會將渲染代理設置為一個不同的 has
handler 代理陷阱,它的性能更強并且能夠去避免檢測一些全局變量。
至此組件的初始化完畢,渲染函數(shù)也設置結束了。
4.總結
在vue3中,新的setup函數(shù)屬性給我們提供了書寫的便利,其背后的工作量無疑是巨大的,有狀態(tài)的組件的初始化的過程,在 setup 函數(shù)初始化部分我們討論的源碼的執(zhí)行過程,我們不僅學習了 setup 上下文初始化的條件,也明確的知曉了 setup 上下文究竟給我們暴露了哪些屬性,并且從中學到了一個新的 RFC 提案屬性: expose 屬性
我們學習了 setup 函數(shù)執(zhí)行的過程以及 Vue 是如何處理捕獲 setup 的返回結果的。
然后我們講解了組件初始化時,不論是否使用 setup 都會執(zhí)行的 finishComponentSetup 函數(shù),通過這個函數(shù)內部的邏輯我們了解了一個組件在初始化完畢時,渲染函數(shù)設置的規(guī)則。
- vue3中setup語法糖下父子組件間傳遞數(shù)據(jù)的方式
- Vue3.2中setup語法糖的使用教程分享
- vue3中<script?setup>?和?setup函數(shù)的區(qū)別對比
- Vue3?setup的注意點及watch監(jiān)視屬性的六種情況分析
- vue3在setup中使用mapState解讀
- Vue3中關于setup與自定義指令詳解
- Vue3中的setup語法糖、computed函數(shù)、watch函數(shù)詳解
- Vue3?setup?的作用實例詳解
- Vue3?setup添加name的方法步驟
- Vue3的setup在el-tab中動態(tài)加載組件的方法
- vue3.0?setup中使用vue-router問題
- vue3中setup語法糖下通用的分頁插件實例詳解
- vue3?setup語法糖各種語法新特性的使用方法(vue3+vite+pinia)
相關文章
vue報錯Error:Cannot?find?module?'fs/promises'的解決方
最近的項目需要用到vue/cli,但是用cnpm安裝vue/cli的時候報錯了,下面這篇文章主要給大家介紹了關于vue報錯Error:Cannot?find?module?'fs/promises'的解決方式,需要的朋友可以參考下2022-11-11使用vue初用antd 用v-model來雙向綁定Form表單問題
這篇文章主要介紹了使用vue初用antd 用v-model來雙向綁定Form表單問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04vue2.0實現(xiàn)移動端的輸入框實時檢索更新列表功能
最近小編在做vue2.0的項目,遇到移動端實時檢索搜索更新列表的效果,下面腳本之家小編給大家?guī)砹藇ue2.0 移動端的輸入框實時檢索更新列表功能的實例代碼,感興趣的朋友參考下吧2018-05-05