Vue3 script setup 語法糖詳解
theme: cyanosis
highlight: zenburn
目前setup sugar
已經進行了定稿,vue3 + setup sugar + TS
的寫法看起來很香,寫本文時 Vue 版本是"^3.2.6"
script setup 語法糖
新的setup
選項是在元件建立之前,props
被解析之後執行,是組合式 API 的入口。
WARNING\
在setup
中你應該避免使用this
,因為它不會找到元件例項。setup
的呼叫發生在data
property、computed
property 或methods
被解析之前,所以它們無法>在setup
中被獲取。
setup
選項是一個接收props
context
的函式,我們將在之後進行討論。此外,我們將setup
返回的所有內容都暴露給元件的其餘部分 (計算屬性、方法、生命週期鉤子等等) 以及元件的模板。
它是 Vue3 的一個新語法糖,在setup
函式中。所有 ES 模組匯出都被認為是暴露給上下文的值,幷包含在 setup() 返回物件中。相對於之前的寫法,使用後,語法也變得更簡單。
在添加了setup的script標籤中,我們不必宣告和方法,這種寫法會自動將所有頂級變數、函式,均會自動暴露給模板(template)使用\
這裡強調一句 “暴露給模板,跟暴露給外部不是一回事”
使用方式極其簡單,僅需要在script
標籤加上setup
<script setup></script> |
該setup功能是新的元件選項。它是元件內部暴露出所有的屬性和方法的統一API。
使用後意味著,script標籤內的內容相當於原本元件宣告中setup()的函式體,不過也有一定的區別。
使用 script setup 語法糖,元件只需引入不用註冊,屬性和方法也不用返回,也不用寫setup函式,也不用寫export default ,甚至是自定義指令也可以在我們的template中自動獲得。基本語法
呼叫時機
建立元件例項,然後初始化 props ,緊接著就呼叫setup 函式。從生命週期鉤子的視角來看,它會在 beforeCreate 鉤子之前被呼叫.
模板中使用
如果 setup 返回一個物件,則物件的屬性將會被合併到元件模板的渲染上下文
<template> | |
<div> | |
{{count}}{{object.foo}} | |
</div> | |
</template> |
setup 引數
- 「props」第一個引數接受一個響應式的props,這個props指向的是外部的props。如果你沒有定義props選項,setup中的第一個引數將為undifined。props和vue2.x並無什麼不同,仍然遵循以前的原則;
- 不要在子元件中修改props;如果你嘗試修改,將會給你警告甚至報錯。
- 不要結構props。結構的props會失去響應性。
2.「context」第二個引數提供了一個上下文物件,從原來 2.x 中 this 選擇性地暴露了一些 property。
<script setup="props, context" lang="ts"> | |
context.attrs | |
context.slots | |
context.emit | |
<script> |
像這樣,只要在setup處宣告即可自動匯入,同時也支援解構語法:
<script setup="props, { emit }" lang="ts"> | |
<script> |
元件自動註冊
匯入 component 或 directive 直接import即可,無需額外宣告
import { MyButton } from "@/components" | |
import { directive as clickOutside } from 'v-click-outside' |
與原先一樣,模板中也支援使用kabab-case來建立元件,如<my-button />
在 script setup 中,引入的元件可以直接使用,無需再通過components
進行註冊,並且無法指定當前元件的名字,它會自動以檔名為主,也就是不用再寫name
屬性了。示例:
<template> | |
<HelloWorld /> | |
</template> | |
<script setup> | |
import HelloWorld from "./components/HelloWorld.vue"; //此處使用 Vetur 外掛會報紅 | |
</script> |
如果需要定義類似 name 的屬性,可以再加個平級的 script 標籤,在裡面實現即可。
元件核心 API 的使用
定義元件的 props
通過defineProps
指定當前 props 型別,獲得上下文的props物件。示例:
<script setup> | |
import { defineProps } from 'vue' | |
const props = defineProps({ | |
title: String, | |
}) | |
</script> | |
<!-- 或者 --> | |
<script setup lang="ts"> | |
import { ref,defineProps } from 'vue'; | |
type Props={ | |
msg:string | |
} | |
defineProps<Props>(); | |
</script> |
定義 emit
使用defineEmit
定義當前元件含有的事件,並通過返回的上下文去執行 emit。示例:
<script setup> | |
import { defineEmits } from 'vue' | |
const emit = defineEmits(['change', 'delete']) | |
</script> |
父子元件通訊
defineProps用來接收父元件傳來的 props ;defineEmits用來宣告觸發的事件。
//父元件 | |
<template> | |
<my-son foo="??????" @childClick="childClick" /> | |
</template> | |
<script lang="ts" setup> | |
import MySon from "./MySon.vue"; | |
let childClick = (e: any):void => { | |
console.log('from son:',e); //?????? | |
}; | |
</script> | |
//子元件 | |
<template> | |
<span @click="sonToFather">資訊:{{ props.foo }}</span> | |
</template> | |
<script lang="ts" setup> | |
import { defineEmits, defineProps} from "vue"; | |
const emit = defineEmits(["childClick"]); // 宣告觸發事件 childClick | |
const props = defineProps({ foo: String }); // 獲取props | |
const sonToFather = () =>{ | |
emit('childClick' , props.foo) | |
} | |
</script> |
子元件通過 defineProps 接收父元件傳過來的資料,子元件通過 defineEmits 定義事件傳送資訊給父元件
增強的props型別定義:
const props = defineProps<{ | |
foo: string | |
bar?: number | |
}>() | |
const emit = defineEmit<(e: 'update' | 'delete', id: number) => void>() |
不過注意,採用這種方法將無法使用props預設值。
定義響應變數、函式、監聽、計算屬性computed
<script setup lang="ts"> | |
import { ref,computed,watchEffect } from 'vue'; | |
const count = ref(0); //不用 return ,直接在 templete 中使用 | |
const addCount=()=>{ //定義函式,使用同上 | |
count.value++; | |
} | |
//定義計算屬性,使用同上 | |
const howCount=computed(()=>"現在count值為:"+count.value); | |
//定義監聽,使用同上 //...some code else | |
watchEffect(()=>console.log(count.value)); | |
</script> |
watchEffect
用於有副作用的操作,會自動收集依賴。
和watch區別
無需區分deep,immediate,只要依賴的資料發生變化,就會呼叫
reactive
此時name只會在初次建立的時候進行賦值,如果中間想要改變name的值,那麼需要藉助composition api 中的reactive。
<script setup lang="ts"> | |
import { reactive, onUnmounted } from 'vue' | |
const state = reactive({ | |
counter: 0 | |
}) | |
// 定時器 每秒都會更新資料 | |
const timer = setInterval(() => { | |
state.counter++ | |
}, 1000); | |
onUnmounted(() => { | |
clearInterval(timer); | |
}) | |
</script> | |
<template> | |
<div>{{state.counter}}</div> | |
</template> |
使用ref也能達到我們預期的'counter',並且在模板中,vue進行了處理,我們可以直接使用counter而不用寫counter.value.
ref和reactive的關係:
ref是一個{value:'xxxx'}的結構,value是一個reactive物件
ref 暴露變數到模板
曾經的提案中,如果需要暴露變數到模板,需要在變數前加入export宣告:
export const count = ref(0) |
不過在新版的提案中,無需export宣告,編譯器會自動尋找模板中使用的變數,只需像下面這樣簡單的宣告,即可在模板中使用該變數
<script setup lang="ts"> | |
import { ref } from 'vue' | |
const counter = ref(0);//不用 return ,直接在 templete 中使用 | |
const timer = setInterval(() => { | |
counter.value++ | |
}, 1000) | |
onUnmounted(() => { | |
clearInterval(timer); | |
}) | |
</script> | |
<template> | |
<div>{{counter}}</div> | |
</template> |
生命週期方法
因為setup
是圍繞beforeCreate
和created
生命週期鉤子執行的,所以不需要顯式地定義它們。換句話說,在這些鉤子中編寫的任何程式碼都應該直接在setup
函式中編寫。
可以通過在生命週期鉤子前面加上 “on” 來訪問元件的生命週期鉤子。官網:生命週期鉤子
下表包含如何在setup ()內部呼叫生命週期鉤子:
選項式 API | Hook insidesetup |
---|---|
beforeCreate |
Not needed* |
created |
Not needed* |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
activated |
onActivated |
deactivated |
onDeactivated |
<script setup lang="ts"> | |
import { onMounted } from 'vue'; | |
onMounted(() => { console.log('mounted!'); }); | |
</script> |
獲取 slots 和 attrs
注:useContext API 被棄用,取而代之的是更加細分的 api。
可以通過useContext
從上下文中獲取 slots 和 attrs。不過提案在正式通過後,廢除了這個語法,被拆分成了useAttrs
和useSlots
。
useAttrs
:見名知意,這是用來獲取 attrs 資料,但是這和 vue2 不同,裡面包含了class
、屬性
、方法
。
<template> | |
<component v-bind='attrs'></component> | |
</template> | |
<srcipt setup lang='ts'> | |
const attrs = useAttrs(); | |
<script> |
useSlots
: 顧名思義,獲取插槽資料。
使用示例:
// 舊 | |
<script setup> | |
import { useContext } from 'vue' | |
const { slots, attrs } = useContext() | |
</script> | |
// 新 | |
<script setup> | |
import { useAttrs, useSlots } from 'vue' | |
const attrs = useAttrs() | |
const slots = useSlots() | |
</script> |
其他 Hook Api
useCSSModule
:CSS Modules 是一種 CSS 的模組化和組合系統。vue-loader 整合 CSS Modules,可以作為模擬 scoped CSS。允許在單個檔案元件的setup
中訪問CSS模組。此 api 本人用的比較少,不過多做介紹。useCssVars
: 此 api 暫時資料比較少。介紹v-bind in styles
時提到過。useTransitionState
: 此 api 暫時資料比較少。useSSRContext
: 此 api 暫時資料比較少。
defineExpose API
傳統的寫法,我們可以在父元件中,通過 ref 例項的方式去訪問子元件的內容,但在 script setup 中,該方法就不能用了,setup 相當於是一個閉包,除了內部的template
模板,誰都不能訪問內部的資料和方法。
如果需要對外暴露 setup 中的資料和方法,需要使用 defineExpose API。示例:
const a = 1 | |
const b = ref(2) | |
defineExpose({ a, b, }) |
注意:目前發現
defineExpose
暴露出去的屬性以及方法都是unknown
型別,如果有修正型別的方法,歡迎評論區補充。
//父元件
<template>
<Daughter ref="daughter" />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Daughter from "./Daughter.vue";
const daughter = ref(null)
console.log('????~daughter',daughter)
</script>
//子元件
<template>
<div>妾身{{ msg }}</div>
</template>
<script lang="ts" setup>
import { ref ,defineExpose} from "vue";
const msg = ref('貂蟬')
defineExpose({
msg
})
</script>
### 屬性和方法無需返回,直接使用! | |
這可能是帶來的較大便利之一,在以往的寫法中,定義資料和方法,都需要在結尾 return 出去,才能在模板中使用。在 script setup 中,定義的屬性和方法無需返回,可以直接使用!示例: | |
```html | |
<template> | |
<div> | |
<p>My name is {{name}}</p> | |
</div> | |
</template> | |
<script setup> | |
import { ref } from 'vue'; | |
const name = ref('Sam') | |
</script> |
支援 async await 非同步
注意在vue3的原始碼中,setup執行完畢,函式 getCurrentInstance 內部的有個值會釋放對 currentInstance 的引用,await 語句會導致後續程式碼進入非同步執行的情況。所以上述例子中最後一個 getCurrentInstance() 會返回 null,建議使用變數儲存第一個 getCurrentInstance() 返回的引用.
<script setup> | |
const post = await fetch(`/api/post/1`).then((r) => r.json()) | |
</script> |
<script setup>
中可以使用頂層await
。結果程式碼會被編譯成async setup()
:
<script setup> | |
const post = await fetch(`/api/post/1`).then(r => r.json()) | |
</script> |
另外,await 的表示式會自動編譯成在await
之後保留當前元件例項上下文的格式。
注意\
async setup()
必須與Suspense
組合使用,Suspense
目前還是處於實驗階段的特性。我們打算在將來的某個釋出版本中開發完成並提供文件 - 如果你現在感興趣,可以參照tests看它是如何工作的。
定義元件其他配置
配置項的缺失,有時候我們需要更改元件選項,在setup
中我們目前是無法做到的。我們需要在上方
再引入一個script
,在上方寫入對應的export
即可,需要單開一個 script。
<script setup>
可以和普通的<script>
一起使用。普通的<script>
在有這些需要的情況下或許會被使用到:
- 無法在
<script setup>
宣告的選項,例如inheritAttrs
或通過外掛啟用的自定義的選項。 - 宣告命名匯出。
- 執行副作用或者建立只需要執行一次的物件。
在script setup 外使用export default,其內容會被處理後放入原元件宣告欄位。
<script> | |
// 普通 `<script>`, 在模組範圍下執行(只執行一次) | |
runSideEffectOnce() | |
// 宣告額外的選項 | |
export default { | |
name: "MyComponent", | |
inheritAttrs:false, | |
customOptions:{} | |
} | |
</script> | |
<scriptsetup> | |
import HelloWorld from '../components/HelloWorld.vue' | |
// 在 setup() 作用域中執行 (對每個例項皆如此) | |
//yourcode | |
</script> | |
<template> | |
<div> | |
<HelloWorld msg="Vue3 + TypeScript + Vite"/> | |
</div> | |
</template> |
注意:Vue 3 SFC 一般會自動從元件的檔名推斷出元件的 name。在大多數情況下,不需要明確的 name 宣告。唯一需要的情況是當你需要
<keep-alive>
包含或排除或直接檢查元件的選項時,你需要這個名字。
關於 TS 與 ESLint 的不完美
-
與
@typescript-eslint/no-unused-vars
規則不相容,此規則含義為定義了,未進行使用。該規則其實影響不大,關閉即可。 -
與匯入的型別宣告不相容,當你通過解構的方式去匯入型別,
setup sugar
會進行自動匯出。這時候,你就會收到 TS 的一條報錯:此為型別,但被當作值使用。解決辦法:型別匯出使用export default
匯出或者引入時使用import * as xx
來進行引入,也可以使用import type { test } from "./test";
解決。
語法糖實現
vue檔案程式碼
<template> | |
<div>{{ msg }}</div> | |
</template> | |
<script setup> | |
const msg = 'Hello!' | |
</script> |
編譯後的js程式碼:
export default { | |
setup() { | |
const msg = 'Hello!' | |
return function render() { | |
// has access to everything inside setup() scope | |
// 在函式 setup 作用域,函式 render 能訪問 setup 的一切, | |
return h('div', msg) | |
} | |
} | |
} |
注意到,即使普通變數也能作為模版被置入 template 中被編譯,某些人認為這不合適,不夠分離。
vscode配套外掛
volar是一個vscode外掛,用來增強vue編寫體驗,使用volar外掛可以獲得script setup語法的最佳支援。
與vetur
相同,volar
是一個針對vue
的vscode
外掛,不過與vetur
不同的是,volar
提供了更為強大的功能,讓人直呼臥槽
。
安裝的方式很簡單,直接在vscode
的外掛市場搜尋volar
,然後點選安裝就可以了。
vscode
中使用的時候,先禁用Vetur
,再下載使用Volar
使用習慣
從options api
切換到composition api
最大的問題無異於最大的問題就是沒有強制的程式碼分割槽,如果書寫的人沒有很好的程式碼習慣,那麼後續的人將會看的十分難受。目前我是這麼解決的:
-
自我程式碼分割槽並且儘量抽離方法(寫好註釋),分割槽如下:
- 相關引入
- 響應式資料、props、emit 定義
- 生命週期以及 watch 書寫
- 方法定義
- 方法、屬性暴露
-
元件抽離:將頁面拆成兩個資料夾,一個為
views
,一個為components
。views 和 components 資料夾下有各自的檔案。views 資料夾中為頁面入口,掌管資料,而 components 則為頁面中一些元件抽離。如果是公共元件,再抽離到 components 資料夾下其他位置。 -
hook 抽離:儘可能將邏輯抽離,並不一定要進行復用。
寫在最後
寫作不易,希望可以獲得你的一個「贊」。如果文章對你有用,可以選擇「關注 + 收藏」。 如有文章有錯誤或建議,歡迎評論指正,謝謝你。❤️