前言
打開(kāi) Vue3 的官方文檔,它首先會(huì)告訴你,Vue 的組件可以按兩種不同的風(fēng)格書(shū)寫(xiě):選項(xiàng)式 API 和組合式 API。文檔為我們提供一系列兩種風(fēng)格的代碼參考,供我們按照偏好進(jìn)行選擇。
實(shí)際上,Vue3 組件可不止兩種寫(xiě)法,而是多達(dá)十幾種!然而,不管是什么寫(xiě)法,它們都是基于同一個(gè)底層系統(tǒng)實(shí)現(xiàn)的,概念之間也是彼此相通的,只是使用的接口不同。在實(shí)際開(kāi)發(fā)中,我們也不會(huì)同時(shí)使用到那么多種寫(xiě)法,但是這并不意味著我們不需要去了解這些寫(xiě)法!
如果你仔細(xì)閱讀 Vue3 的文檔,會(huì)發(fā)現(xiàn)一些示例或者 api 看起來(lái)模棱兩可,不知道這些 api 到底有什么用,或者閱讀 Vue 的源碼時(shí),總是能發(fā)現(xiàn)一些對(duì)于我們來(lái)說(shuō)意圖不明的邏輯,那么,你可能先要了解一些 Vue 的寫(xiě)法。先了解一個(gè)東西怎么用,再去分析它是怎么實(shí)現(xiàn)的。
看完本文章,你會(huì)收獲到:
- vue 的渲染原理。
- 什么是 defineComponent、h、creatVnode。
- 渲染函數(shù)和 jsx 的區(qū)別。
- 馬上能夠上手 jsx。
- 輕松閱讀 vue 文檔的所有示例和 api。
- 輕松看懂不同的 vue 組件寫(xiě)法。
- 不管新手老手,都會(huì)對(duì) Vue 有所新的認(rèn)識(shí)。
- 等等...
本文章遵從循序漸進(jìn)的寫(xiě)作順序,從易到難,輕松上手!
setup 語(yǔ)法糖
setup 語(yǔ)法糖應(yīng)該是最常用的寫(xiě)法了。在 Vue3 中,我們想封裝一個(gè)組件,最習(xí)慣的做法還是新建一個(gè) Vue 文件,并將組件代碼寫(xiě)在文件中。具體是:頁(yè)面結(jié)構(gòu)寫(xiě)在 template 中,頁(yè)面邏輯寫(xiě)在 script 中,頁(yè)面樣式寫(xiě)在 style 中。
總之,我們將與該組件相關(guān)的代碼都寫(xiě)在一起、放在一個(gè)文件中單獨(dú)維護(hù),在需要該組件的地方引入使用。
這里我們使用了 setup 語(yǔ)法糖,直接在 script 中書(shū)寫(xiě)我們的 setup 內(nèi)部的邏輯。
<template>
<div>{{ name }}</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const name = ref("天氣好");
</script>
<style scoped></style>
在 App. vue 中引入并使用:
// App.vue
<template>
<User />
</template>
<script setup lang="ts">
import User from "./User.vue";
</script>
<style scoped></style>
注:后續(xù)寫(xiě)法盡管形式不同,但它們最終的目的都是導(dǎo)出一個(gè)組件,所以對(duì)于組件使用方來(lái)說(shuō)(這里是 App. vue),怎么使用這個(gè)組件的代碼都是不變的,所以將不再重復(fù)此代碼。
Vue2 選項(xiàng)式寫(xiě)法
Vue2 經(jīng)典寫(xiě)法
這種寫(xiě)法也是比較經(jīng)典的。和 setup 語(yǔ)法糖寫(xiě)法類似。我們需要新建一個(gè) vue 文件來(lái)存儲(chǔ)我們的組件代碼,然后在需要使用該組件的地方對(duì)其進(jìn)行引入。區(qū)別在于,我們需要在 script 中導(dǎo)出一個(gè) Vue 實(shí)例。
這里我們導(dǎo)出的其實(shí)是一個(gè)普通對(duì)象,該對(duì)象包含 data、methods 等屬性。這個(gè)對(duì)象的屬性都是可選的,即 option,翻譯回來(lái)即“選項(xiàng)”。
<template>
<div>{{ name }}</div>
</template>
<script lang="ts">
export default {
data: () => {
return {
name: "天氣好",
};
},
};
</script>
<style></style>
defineComponent 輔助函數(shù)
盡管我們?cè)?script 語(yǔ)言塊中導(dǎo)出的默認(rèn)對(duì)象會(huì)被 vue 編譯器當(dāng)成 vue 實(shí)例,但不管怎么看,它依舊只是一個(gè) plain object。在定義組件實(shí)例方面,vue 提供了一個(gè)名為 defineComponent 輔助接口。
<template>
<div>{{ name }}</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data: () => {
return {
name: "天氣好",
};
},
});
</script>
<style></style>
盡管這個(gè)接口也不能改變我們導(dǎo)出的是一個(gè)普通對(duì)象的事實(shí),但是它可以為我們的實(shí)例提供強(qiáng)大的類型推導(dǎo)。我們可以把它看成是一個(gè)返回 vue 實(shí)例的工廠函數(shù),讓我們的代碼看起來(lái)更加規(guī)范。
Vue3 選項(xiàng)式寫(xiě)法
在 Vue3 中,官方引入了新的選項(xiàng) setup,這是 Vue3 選項(xiàng)式寫(xiě)法和 Vue2 寫(xiě)法的主要區(qū)別。setup 選項(xiàng)的意義在于它允許我們?cè)谶x項(xiàng)式的寫(xiě)法中引用和使用組合式的 api,比如 onMounted、ref、reactive 等。但對(duì)于我們來(lái)說(shuō),它對(duì)于我們有益的地方還是基于它封裝起來(lái)的 setup 語(yǔ)法糖用起來(lái)很方便。
<template>
<div>{{ name }}</div>
</template>
<script lang="ts">
export default {
setup() {
return {
name: "天氣好",
};
},
};
</script>
<style></style>
使用 defineComponent 時(shí),它能夠提示我們 setup 將會(huì)接收到什么參數(shù):
<template>
<div>{{ name }}</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
setup(prop,context) {
return {
name: "天氣好",
};
},
});
</script>
<style></style>
以上寫(xiě)法我們都是在 template 上書(shū)寫(xiě)我們的頁(yè)面結(jié)構(gòu),這也是最常見(jiàn)的幾種寫(xiě)法,下面我們來(lái)介紹幾種了解 vue 底層必不可少的寫(xiě)法,渲染函數(shù)。
手寫(xiě)渲染函數(shù)
template 模板語(yǔ)法本質(zhì)上也可以算是一種語(yǔ)法糖。在 vue 編譯器上,template 中的內(nèi)容最終會(huì)被翻譯為渲染函數(shù),掛載到 vue 實(shí)例的 render 屬性上。當(dāng)需要渲染組件時(shí),vue 就執(zhí)行一次 render,得到對(duì)應(yīng)的虛擬節(jié)點(diǎn)樹(shù),最后再轉(zhuǎn)變?yōu)檎鎸?shí) dom。
Vue 允許我們脫離 template,直接自己書(shū)寫(xiě)渲染函數(shù)。位置就在導(dǎo)出實(shí)例的 render 選項(xiàng)上:
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data: () => ({ name: "天氣好" }),
render() {
return this.name;
},
});
</script>
<style></style>
在 template 中,我們使用類似 html 的模板語(yǔ)法來(lái)描述我們的視圖,在 render 函數(shù)中又如何描述呢?vue 提供了兩個(gè) api:createVnode 和 h。二者沒(méi)有區(qū)別,h 函數(shù)只是 createVnode 的縮寫(xiě)。有了 render 函數(shù),我們就不需要寫(xiě) template 了。
<script lang="ts">
import { defineComponent, h } from "vue";
export default defineComponent({
data: () => ({ name: "天氣好" }),
render() {
return h("div", this.name);
},
});
</script>
<style></style>
在上面的示例中,我們使用 h 函數(shù)生成了一個(gè) vNode,并 return 出去,作為本組件最終在被使用時(shí)渲染出來(lái)的效果。
在 template 中我們可以使用 v-if、v-for、slot 等模板語(yǔ)法,在 h 函數(shù)中這些概念也是支持的,只是形式不同,這方面官方文檔有具體的示例??傊?,template 模板和 render 選項(xiàng)是可以相互替代的。
setup 返回渲染函數(shù)
setup 返回 render 方法
一般來(lái)說(shuō),在選項(xiàng)式語(yǔ)法中,setup 方法返回一個(gè)對(duì)象,該對(duì)象暴露給 template,供 template 使用,具體參考第三個(gè)例子(vue3 選項(xiàng)式寫(xiě)法)。如果我們不使用 template,也就沒(méi)有返回對(duì)象的必要了。
在 Vue3 中,還有另外一種不使用 template 的寫(xiě)法,就是在 setup 方法中返回一個(gè) render 方法。
<script lang="ts">
import { defineComponent, h, ref } from "vue";
export default defineComponent({
setup() {
const name = ref("天氣好");
return () => h("div", name.value);
},
});
</script>
<style></style>
注意:
- 在選項(xiàng)式中使用 setup 之后,一般不應(yīng)該再使用 data、生命周期等在選項(xiàng)式寫(xiě)法中常用的選項(xiàng),而應(yīng)該把主要邏輯都寫(xiě)在 setup 中,并適當(dāng)引入組合式的 api。比如,使用 ref,而不是 data 選項(xiàng)。
- ref 自動(dòng)解包是 template 特有的功能,h 函數(shù)是沒(méi)有這個(gè)功能的。在 h 函數(shù)中引入 ref,記得理所當(dāng)然地帶上
.value
。
defineComponent 傳入setup
就注意中的第一點(diǎn),我們可以采用下面這種寫(xiě)法:直接在 defineComponent 中書(shū)寫(xiě) setup 函數(shù)(如果再省一點(diǎn)就是 setup 語(yǔ)法糖的寫(xiě)法了)。
<script lang="ts">
import { defineComponent, h, ref } from "vue";
export default defineComponent(() => {
const name = ref("天氣好");
return () => h("div", name.value);
});
</script>
<style></style>
以上就是渲染函數(shù)的寫(xiě)法,是不是有點(diǎn)感覺(jué)了呢,一下子就學(xué)會(huì)了兩個(gè) api!后面會(huì)提到的 Jsx 寫(xiě)法其實(shí)也應(yīng)該歸為渲染函數(shù)寫(xiě)法的一種(只要不是 template,而是用 JavaScript 表達(dá)頁(yè)面結(jié)構(gòu)的,都是渲染函數(shù)),但是相對(duì)于 h 函數(shù),jsx 并不是純粹的 js,所以我將它們分成了兩類。
Vue & Jsx
在render 中使用 jsx
有了前面兩類寫(xiě)法介紹的鋪墊,接下來(lái)引入 jsx 語(yǔ)法就沒(méi)有什么難理解的點(diǎn)了。
jsx 在 vue 文件中是這樣寫(xiě)的。在 render 渲染函數(shù)返回值處書(shū)寫(xiě) jsx 替代 h 函數(shù)。書(shū)寫(xiě)純 JavaScript 的 h 函數(shù)描述結(jié)構(gòu)還是比較繁冗的,jsx 就是簡(jiǎn)化了的h 函數(shù)寫(xiě)法。
<script lang="tsx">
import { defineComponent } from "vue";
export default defineComponent({
data() {
return { name: "天氣好" };
},
render() {
return (
<>
<div>{this.name}</div>
</>
);
},
});
</script>
<style></style>
在 setup 中使用jsx
jsx 和 setup 配合食用更加。在選項(xiàng)式風(fēng)格中使用 setup,在 setup 中使用組合式 api,并且返回 jsx 書(shū)寫(xiě)的渲染函數(shù)。
<script lang="tsx">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const name = ref("天氣好");
return () => <>{name.value}</>;
},
});
</script>
<style></style>
defineComponent 簡(jiǎn)寫(xiě)
這個(gè)其實(shí)就是前面介紹過(guò)的 defineComponent 傳入 setup 函數(shù)寫(xiě)法:這里的區(qū)別只是使用 jsx 替代了 h 函數(shù)。
<script lang="tsx">
import { defineComponent, ref } from "vue";
export default defineComponent(() => {
const name = ref("天氣好");
return () => (
<>
<div>{name.value}</div>
</>
);
});
</script>
<style></style>
自行導(dǎo)出 vNode 對(duì)象
我們也可以自己將 render 函數(shù)執(zhí)行一遍,然后將得到的 jsx Element 導(dǎo)出,和上一個(gè)示例defineComponent 簡(jiǎn)寫(xiě)是十分相似。但是這段代碼的缺點(diǎn)非常致命,它不支持接收外部傳遞來(lái)的屬性參數(shù)。
<script lang="tsx">
import { ref } from "vue";
export default (() => {
const name = ref("天氣好");
return () => (
<>
<div>{name.value}</div>
</>
);
})();
</script>
<style></style>
不要使用這種寫(xiě)法。這里會(huì)提到這樣寫(xiě),只是因?yàn)楹秃竺娴?strong>函數(shù)式組件(其二) 寫(xiě)法有關(guān)聯(lián)。本寫(xiě)法與其它寫(xiě)法都不同,其它寫(xiě)法導(dǎo)出的都是 JavaScript 對(duì)象或者 jsx 對(duì)象,而這里我們則是自己執(zhí)行了一遍渲染函數(shù)并得到了虛擬節(jié)點(diǎn),直接將虛擬節(jié)點(diǎn)導(dǎo)出去。既然都已經(jīng)把虛擬節(jié)點(diǎn)創(chuàng)建出來(lái)了,那自然無(wú)法接收 props。
defineComponent 的第二個(gè)參數(shù)
如果 defineComponent 的第一個(gè)參數(shù)是 setup 函數(shù),那么它的第二個(gè)參數(shù)則可以為組件的定義添加需要的選項(xiàng),但一般除了補(bǔ)充 props 選項(xiàng),不會(huì)再需要其它選項(xiàng)了(組合式 api 和 setup 的入?yún)⒖梢酝耆娲渌x項(xiàng))。
<script lang="tsx">
import { defineComponent } from "vue";
export default defineComponent(
(props) => {
return () => (
<>
<div>{props.userName}</div>
</>
);
},
{
props: { userName: String },
}
);
</script>
<style></style>
直接在 vue 中使用 jsx
這里 jsx 不再只作為返回值,而是直接被某處使用。它可以是被直接導(dǎo)出,或者用在 template 上。
直接導(dǎo)出 jsx 對(duì)象
直接將 jsx 對(duì)象導(dǎo)出使用。比前面的寫(xiě)法更簡(jiǎn)潔,做法就是把 setup 里面的內(nèi)容提到外面。這里需要注意的是我們導(dǎo)出的是一段直接的 jsx 對(duì)象(jsx Element),而不是渲染函數(shù)。
<script lang="tsx">
import { ref } from "vue";
const name = ref("天氣好");
const User = <>{name.value}</>;
export default User;
</script>
<style scoped></style>
直接用在 template 上
這種寫(xiě)法可以幫助你在自身的組件內(nèi)復(fù)用一些顆粒度更小的組件,它和 setup 語(yǔ)法糖的寫(xiě)法非常接近,只是 User 變量可以作為標(biāo)簽直接使用。
<template>
<User />
</template>
<script setup lang="tsx">
import { ref } from "vue";
const name = ref("天氣好");
const User = <>{name.value}</>;
</script>
<style></style>
函數(shù)式組件(其一)
你還可以將 User 寫(xiě)成函數(shù)式組件,在本頁(yè)面內(nèi)使用。但它不會(huì)將連字符屬性轉(zhuǎn)換為小駝峰寫(xiě)法。這和直接用在 template 上的內(nèi)容都是一樣的,它們都是為了方便在組件本身復(fù)用一些常用的組件。
<template>
<User :user-name="name" />
</template>
<script setup lang="tsx">
import { ref } from "vue";
const name = ref("天氣好");
const User = (props: { "user-name": string }) => {
return <>{props["user-name"]}</>;
};
</script>
<style></style>
如果你經(jīng)常使用 tailwind,你可能就會(huì)知道什么情況下會(huì)出現(xiàn)小顆粒度的可復(fù)用標(biāo)簽,比如,一個(gè)加了一大堆類名的 div 標(biāo)簽。
獨(dú)立的 Jsx 文件
以上介紹的所有寫(xiě)法,都是在 .vue
文件中書(shū)寫(xiě)的,而且也離不開(kāi) <script lang="tsx">
。接下來(lái)的寫(xiě)法可以讓我們脫離 Vue 的語(yǔ)法塊框架,書(shū)寫(xiě)更像 jsx 的 jsx。
jsx 定義組件
我們需要新建一個(gè) jsx/tsx 文件,然后只要保證導(dǎo)出的仍然是一個(gè)組件就可以了。有了前面的鋪墊,我們不難發(fā)現(xiàn),這不就是去掉 script 標(biāo)簽的選項(xiàng)式寫(xiě)法嗎?確實(shí)!這是因?yàn)槲夜室庠谇懊姘才帕诉x項(xiàng)式寫(xiě)法的例子,所以過(guò)渡到這里完全沒(méi)有壓力!
// User.tsx
import { ref } from 'vue'
export default {
setup() {
const name = ref('天氣好')
return () => <><div>{name.value}</div></>
}
}
我還是推薦套上 defineComponent:
// User.tsx
import { ref, defineComponent } from 'vue'
export default defineComponent({
setup() {
const name = ref('天氣好')
return () => (<><div>{name.value}</div></>)
}
});
同樣地,前面對(duì)于 defineComponent 不同方式的使用這里也都可以的。比如導(dǎo)出普通對(duì)象并在 render 或者 setup 中使用 jsx 等等。從 vue 到 jsx,區(qū)別只是省下了 script 語(yǔ)法塊。
vue2 選項(xiàng)式寫(xiě)法+jsx。
// User.tsx
import { defineComponent } from 'vue'
export default defineComponent({
data: () => ({ name: '天氣好' }),
render() {
return <><div>{this.name}</div></>
}
});
導(dǎo)出普通對(duì)象:
// User.tsx
export default {
data: () => ({ name: '天氣好' }),
render() {
return <><div>{this.name}</div></>
}
});
函數(shù)式組件(其二)
Vue 中支持的最像函數(shù)式組件的寫(xiě)法。
// User.tsx
import { ref } from 'vue'
export default function User(props) {
const name = ref('天氣好')
return <><div>{name.value}</div></>
}
該例和前面的自行導(dǎo)出 vNode 對(duì)象非常接近,這也是為什么即使后者存在不能接收參數(shù)的缺陷我也會(huì)提出來(lái),因?yàn)槎叨际鞘褂媒咏瘮?shù)式組件的寫(xiě)法來(lái)描述組件的,但是在 vue 文件中并沒(méi)有辦法直接導(dǎo)出這個(gè)函數(shù)組件,而是需要自行執(zhí)行得到vNode。而在 jsx 文件中卻可以將其導(dǎo)出,并且支持接收參數(shù)。
如果你需要為其定義 props,也不需要使用 defineComponent 的第二個(gè)參數(shù)為你提供什么 props 選項(xiàng),而是直接在函數(shù)式組件的 props 、emits 屬性上掛載對(duì)應(yīng)的配置。
import { ref } from 'vue'
User.props = {
userName: String
}
function User(props) {
// const name = ref('天氣好')
return <><div>{props.userName}</div></>
}
export default User;
相信習(xí)慣了 React 的 fc 的小伙伴,看到這里一定感覺(jué)倍感親切。然而 Vue 的 Jsx 終究只是 Vue 的 Jsx,它并沒(méi)有像 React 一樣存在那么多強(qiáng)大的 Hooks 和內(nèi)置組件,而是僅僅只是 h 函數(shù)的便捷寫(xiě)法。在語(yǔ)法上也和 React Jsx 存在諸多區(qū)別。和 React Jsx 相比,Vue Jsx 其實(shí)和自家的 template 更接近。不過(guò) Vue Jsx 寫(xiě)法的靈活性還是要比 template 模板高,但官方更推薦使用 template。template 更容易上手且提供了更好的性能優(yōu)化,除非你想完完全全掌控組件的每一個(gè)細(xì)節(jié),才需要jsx。
小結(jié)
盡管本文提到了很多種寫(xiě)法,但大多數(shù)寫(xiě)法在大多數(shù)時(shí)候都是不會(huì)派上用場(chǎng)的,也應(yīng)該不被派上用場(chǎng)。之所以列舉那么多寫(xiě)法,主要的目的還是為了循序漸進(jìn)引入 jsx 文件寫(xiě)法和函數(shù)式組件寫(xiě)法。
可以看出,Vue 的寫(xiě)法本質(zhì)上是選項(xiàng)式的。Vue3 在 Vue2 的基礎(chǔ)上引入了 setup 選項(xiàng)和 setup 語(yǔ)法糖,結(jié)合組合式 api 后,開(kāi)發(fā)者可以將組件的大部分邏輯都維護(hù)在 setup 中,而不是 vue2 中割裂了邏輯的 data+created+methods 選項(xiàng)。
在此基礎(chǔ)上,setup 語(yǔ)法糖支持自動(dòng)導(dǎo)出組件功能,為我們?nèi)粘i_(kāi)發(fā)帶來(lái)了很大的便利。
但是除了使用 template 來(lái)表達(dá)我們的結(jié)構(gòu),我們也可以自己使用 render 選項(xiàng)并借助 h 函數(shù)或者 jsx 的力量來(lái)手寫(xiě)渲染函數(shù)。這些都是在 vue 文件中完成的。
既然都不需要 template 了,那么 vue 文件里就只剩下一個(gè) script 了(我們先忽略 style)。在 jsx 文件中,就允許我們直接書(shū)寫(xiě)導(dǎo)出對(duì)象(仍是選項(xiàng)式的寫(xiě)法),忽略script。
最后是 Vue 的 jsx 文件獨(dú)有的特性。它允許我們導(dǎo)出一個(gè)函數(shù)作為組件,我們稱之為函數(shù)式組件(fc,function component),這是 vue 文件和以前所有寫(xiě)法所不具備的,外形與 React 相近。
總的來(lái)說(shuō)就一句話,Vue 本身仍然是選項(xiàng)式的,但是它現(xiàn)在還額外支持了在 jsx 文件中書(shū)寫(xiě) fc。
作者:天氣好