擁抱 Vue 3 系列之 JSX 語(yǔ)法
“別再更新了,學(xué)不動(dòng)了”。這句話(huà)不知道出了多少開(kāi)發(fā)者的辛酸。在過(guò)去的一年中,Vue 團(tuán)隊(duì)一直都在開(kāi)發(fā) Vue.js 的下一個(gè)主要版本,就在 6 月底,尤大更新同步了 Vue 3 及其周邊生態(tài)的狀態(tài):Vue 3: mid 2020 status update (https://github.com/vuejs/rfcs/issues/183)。
if?(isTrue("I?am?planning?to?use?Vue?3?for?a?new?project"))?{
??if?(isTrue("I?need?IE11?support"))?{
????await?IE11CompatBuild()?//?July?2020
??}
??if?(isTrue("RFCs?are?too?dense,?I?need?an?easy-to-read?guide"))?{
????await?migrationGuide()?//?July?2020
??}
??if?(isTrue("I'd?rather?wait?until?it's?really?ready")?{
??????await?finalRelease()?//?Targeting?early?August?2020
??})
??run(`npm?init?vite-app?hello-vue3`)
??return
}
我們可以看到,如果一切順利的話(huà),預(yù)計(jì)在 8 月份,Vue 3 的正式版本就可以和我們見(jiàn)面了,目前距離發(fā)布正式版還有一定的差距,還要做一些兼容性的工作。同時(shí)還會(huì)提供對(duì) IE11 的支持。
全面擁抱 TypeScript
重構(gòu) complier
重構(gòu) Virtual DOM
......
寫(xiě)在前面
比如當(dāng)開(kāi)始寫(xiě)一個(gè)只能通過(guò) level prop 動(dòng)態(tài)生成標(biāo)題 (heading) 的組件時(shí),你可能很快想到這樣實(shí)現(xiàn):
<script?type="text/x-template"?id="anchored-heading-template">
??if
="level?===?1">
????<slot>slot>
??h1>
??<h2?v-else-if="level?===?2">
????<slot>slot>
??h2>
??<h3?v-else-if="level?===?3">
????<slot>slot>
??h3>
script>
這里用模板并不是最好的選擇,在每一個(gè)級(jí)別的標(biāo)題中重復(fù)書(shū)寫(xiě)了 ,不夠優(yōu)雅。
const?App?=?{
??render()?{
????const?tag?=?`h${this.level}`
????return?<tag>{this.$slots.default}tag>
??}
}
Vue JSX 簡(jiǎn)介
const?el?=?<div>Vue?3div>;
這段代碼既不是 HTML 也不是字符串,被稱(chēng)之為 JSX,是 JavaScript 的擴(kuò)展語(yǔ)法。JSX 可能會(huì)使人聯(lián)想到模板語(yǔ)法,但是它具備 Javascript 的完全變成能力。
<div?id="app">{{?msg?}}div>
function?render()?{
??with(this)?{
????return?_c('div',?{
??????attrs:?{
????????"id":?"app"
??????}
????},?[_v(_s(msg))])
??}
}
觀察上述代碼我們發(fā)現(xiàn),到運(yùn)行階段實(shí)際上都是 render 函數(shù)在執(zhí)行。Vue 推薦在絕大多數(shù)情況下使用 template 來(lái)創(chuàng)建你的 HTML。然而在一些場(chǎng)景中,就需要使用 render 函數(shù),它比 template 更加靈活。

使用過(guò) React 的同學(xué)對(duì)于如何寫(xiě) JSX 語(yǔ)法一定非常熟悉了,然而,Vue 2 中 的 JSX 寫(xiě)法和 React 還是有一些略微的區(qū)別。React 中所有傳遞的數(shù)據(jù)都掛在頂層。
const?App?=?<A?className="x"?style={style}?onChange={onChange}?/>
props,普通 html 屬性 attrs,DOM 屬性 domProps。想要更多了解如何在 Vue 2 中寫(xiě) JSX 語(yǔ)法,可以看這篇,在 Vue 中使用 JSX 的正確姿勢(shì) (https://zhuanlan.zhihu.com/p/37920151)。Vue 3 中對(duì) JSX 帶來(lái)的改變
屬性傳遞
//?before
{
??class:?['foo',?'bar'],
??style:?{?color:?'red'?},
??attrs:?{?id:?'foo'?},
??domProps:?{?innerHTML:?''?},
??on:?{?click:?foo?},
??key:?'foo'
}
//?after
{
??class:?['foo',?'bar'],
??style:?{?color:?'red'?},
??id:?'foo',
??innerHTML:?'',
??onClick:?foo,
??key:?'foo'
}
指令改版
v-model、v-show 這些 API 全部通過(guò)模塊導(dǎo)出的方式來(lái)引入“基線(xiàn)體積:無(wú)法舍棄的代碼體積
我們來(lái)看一段非常簡(jiǎn)單的代碼 ,在 Vue 2 和 Vue 3 中的編譯結(jié)果有何不同
//?before
function?render()?{
??with(this)?{
????return?_c('input',?{
??????directives:?[{
????????name:?"model",
????????rawName:?"v-model",
????????value:?(x),
????????expression:?"x"
??????}],
??????domProps:?{
????????"value":?(x)
??????},
??????on:?{
????????"input":?function?($event)?{
??????????if?($event.target.composing)?return;
??????????x?=?$event.target.value
????????}
??????}
????})
??}
}
//?after
import?{?vModelText?as?_vModelText,?createVNode?as?_createVNode,?withDirectives?as?_withDirectives,?openBlock?as?_openBlock,?createBlock?as?_createBlock?}?from?"vue"
export?function?render(_ctx,?_cache)?{
??return?_withDirectives((_openBlock(),?_createBlock("input",?{
????"onUpdate:modelValue":?$event?=>?(_ctx.x?=?$event)
??},?null,?8?/*?PROPS?*/,?["onUpdate:modelValue"])),?[
????[_vModelText,?_ctx.x]
??])
}
可以看到在 Vue 3 中,對(duì)各個(gè) API 做了更加細(xì)致的拆分,理想狀態(tài)下,用戶(hù)可以在構(gòu)建時(shí)利用搖樹(shù)優(yōu)化 (tree-shaking) 去掉框架中不需要的特性,只保留自己用到的特性。模版編譯器會(huì)生成適合做 tree-shaking 的代碼,不需要使用者去關(guān)心如何去做,這部分的改動(dòng)同樣需要在 JSX 寫(xiě)法中實(shí)現(xiàn)。
模板編譯器中增加了 PatchFlag,在 JSX 的編譯過(guò)程同樣也做了處理,性能會(huì)有提升,但是考慮到 JSX 的靈活性,做了一些兼容處理,該功能還在測(cè)試階段。
從 Vue 2 到 Vue 3 的過(guò)渡
Vue 3 雖然引入了一部分破壞性的更新,但對(duì)于絕大多數(shù) Vue 2 的 API 還是兼容的。那么同樣的,我們也要盡可能讓使用 JSX 的用戶(hù)通過(guò)最小的成本升級(jí)到 Vue 3,這是一個(gè)核心的目標(biāo)。寫(xiě)這篇文章的時(shí)候,antdv 已經(jīng)使用 @ant-design-vue/babel-plugin-jsx (https://github.com/vueComponent/ant-design-vue) 重構(gòu)了大約 70% 的功能,預(yù)計(jì)會(huì)在 Vue 3 正式版之前發(fā)布測(cè)試版,大概率會(huì)是東半球最快兼容 Vue 3 的企業(yè)級(jí)組件庫(kù)。
Vue 3 JSX 的 API 設(shè)計(jì)
函數(shù)式組件
const?App?=?()?=>?<div>Vue?3?JSXdiv>
普通組件
const?App?=?{
??render()?{
????return?<div>Vue?3.0div>
??}
}
const?App?=?defineComponent(()?=>?{
??const?count?=?ref(0);
??const?inc?=?()?=>?{
????count.value++;
??};
??return?()?=>?(
????<div?onClick={inc}>
??????{count.value}
????div>
??)
})
Fragment
const?App?=?()?=>?(
??<>
????<span>I'mspan>
????<span>Fragmentspan>
??>
)
Fragment 參考 React 的寫(xiě)法,盡可能寫(xiě)起來(lái)更加方便
Attributes/Props
const?App?=?()?=>?<input?type="email"?/>
const?placeholderText?=?'email'
const?App?=?()?=>?(
??<input
????type="email"
????placeholder={placeholderText}
??/>
)
指令
“建議在 JSX 中使用駝峰 (
vModel),但是v-model也能用
v-show
const?App?=?{
??data()?{
????return?{?visible:?true?};
??},
??render()?{
????return?<input?vShow={this.visible}?/>;
??},
};
v-model
“修飾符:使用 (
_) 代替 (.) (vModel_trim={this.test})
export?default?{
?data:?()?=>?({
???test:?'Hello?World',
?}),
?render()?{
???return?(
?????<>
???????
???????{this.test}
?????>
???)
?},
}
自定義指令
const?App?=?{
??directives:?{?antRef?},
??setup()?{
????return?()?=>?(
??????<a
????????vAntRef={(ref)?=>?{?this.ref?=?ref;?}}
??????/>
????);
??},
}
插槽
關(guān)于指令、插槽最終的 API 還在討論中,有想法的可以去留言。Vue 3 JSX Design (https://github.com/vuejs/jsx/issues/141)
Vue 2 的 JSX 寫(xiě)法如何快速遷移到 Vue 3
{
??"plugins":?["@ant-design-vue/babel-plugin-jsx",?{?"transformOn":?true,?"compatibleProps":?true?}]
}
transformOn
on: { click: xx } 寫(xiě)法的兼容,在運(yùn)行時(shí)中會(huì)轉(zhuǎn)為 onClick: xxxcompatibleProps
props、attrs 這些都不存在了,因此如果設(shè)置了這個(gè)屬性為 true,在運(yùn)行時(shí)也會(huì)被解構(gòu)到第一層的屬性中。createVNode 的第二個(gè)參數(shù),都會(huì)包一個(gè) compatibleProps 和 transformOn 方法,所以酌情開(kāi)啟這兩個(gè)參數(shù)。對(duì)于使用 Vue 2 的 JSX 同學(xué),如果沒(méi)有使用到比較”不為人知“ 的 API的情況下,都可以快速得遷移。compatibleProps 勢(shì)必不太優(yōu)雅,因此沒(méi)有選擇開(kāi)啟這個(gè)兩個(gè)開(kāi)關(guān)。這里插一句,目前 antdv 的遷移還在進(jìn)行中,相關(guān)的進(jìn)度都在這個(gè) issue 里面:Vue 3 支持 (https://github.com/vueComponent/ant-design-vue/issues/1913),有興趣的同學(xué)可以關(guān)注下,提一些 PR 過(guò)去。
如果是通過(guò)對(duì)象來(lái)傳遞的屬性,只需要把原有分散在 props、on、attrs 中的值直接鋪開(kāi)即可。
?const?vcUploadProps?=?{
-??props:?{
-????...this.$props,
-???prefixCls,
-????beforeUpload:?this.reBeforeUpload,
-??},
-??on:?{
-????start:?this.onStart,
-????error:?this.onError,
-????progress:?this.onProgress,
-????success:?this.onSuccess,
-????reject:?this.onReject,
-?},
+??...this.$props,
+??prefixCls,
+??beforeUpload:?this.reBeforeUpload,
+??onStart:?this.onStart,
+??onError:?this.onError,
+??onProgress:?this.onProgress,
+??onSuccess:?this.onSuccess,
+??onReject:?this.onReject,
+??ref:?'uploadRef',
+??attrs:?this.$attrs,
+??...this.$attrs,
};
但是關(guān)于 inheritAttrs 有個(gè)較為底層的變動(dòng),需要開(kāi)發(fā)者根據(jù)實(shí)際情況去修改。什么是inheritAttrs? (https://cn.vuejs.org/v2/api/index.html#inheritAttrs) 在 Vue 2 中,這個(gè)選項(xiàng)不影響 class 和 style 綁定,但是在 Vue 3 中會(huì)影響到。因此可能在屬性的傳遞上,需要額外對(duì)這兩個(gè)參數(shù)做處理。
emit 來(lái)觸發(fā)。例如聲明了 onClick 事件,仍然可以使用 emit('click')。Vue 3 對(duì) context 的 API 也做了改動(dòng),一般如果不是復(fù)雜的組件,不會(huì)涉及到這個(gè) API。這部分的改動(dòng)可以看原先 Vue Compositon API 的相關(guān)文檔,Dependency Injection (https://composition-api.vuejs.org/api.html#dependency-injection),注意一點(diǎn),在 setup 中取不到 this。
總結(jié)
Ant Design Vue https://github.com/vueComponent/ant-design-vue
@ant-design-vue/babel-plugin-jsx https://github.com/vueComponent/jsx
?? 看完三件事
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
關(guān)注我的官網(wǎng)?https://muyiy.cn,讓我們成為長(zhǎng)期關(guān)系
關(guān)注公眾號(hào)「高級(jí)前端進(jìn)階」,公眾號(hào)后臺(tái)回復(fù)「面試題」 送你高級(jí)前端面試題,回復(fù)「加群」加入面試互助交流群
