大家好,我是若川。今天再分享 Vueconf 的一篇文章。另外 Vueconf 主辦方提供的錄播鏈接是:?https://www.bilibili.com/read/mobile?id=11408693,感興趣可以復制觀看。
點擊下方卡片關注我、加個星標。
學習源碼整體架構系列、年度總結、JS基礎系列
本文為 Vue Conf 2021 分享內容。
分享者:林成璋,目前就職于 字節跳動 大力智能前端 團隊
1. 引言
各位同學下午好,我是來自字節跳動大力智能前端團隊的林成璋,最近半年的業余時間(再加上一些摸魚的時間)主要在維護 Vue 3 的 Babel JSX Plugin[1],今天來給大家做一個關于 JSX 的分享。

下面是我的 Github 賬號,全網除了 P 站應該都是這個頭像。其實最早做這個插件主要是為了幫助 Ant Design Vue 和 Vant 能夠快速升級到 Vue 3,看過他們源碼的同學應該知道,他們的源碼大部分都是用 JSX 來擼的。

雖然目前在 NPM 上的周下載量是 56 萬多(甚至超過了 Vue 3 ????),但是這里的下載量非常大的原因主要是通過 vue-cli 創建的項目(不管是 Vue 2 還是 Vue 3)都會下載 @vue/babel-plugin-jsx 這個包,實際使用 JSX 的用戶應該遠比這個數字要小,到底有多少用戶是通過的 JSX 的方式開發的也沒有辦法統計到,絕大用戶還是使用 template 的開發方式為主。

2. 基本概念
template
在 Vue 里,sfc 是一個以 .vue 結尾的文件,通常包含三種類型的頂級語言塊 <template>
、<script>
和 <style>
,可以理解為 HTML 、JS 以及 CSS 的組合。每一個 .vue 文件結尾的文件都是一個組件,而且只能 export defualt 出一個組件。

JSX
本身就是 JS

3. 為什么在 Vue 里也支持 JSX
Vue 官方推薦的開發方式是 template,從 Vue 2 開始,template 在運行之前,會被編譯成 JavaScript 的 render function。這些 render function 在運行時階段,就是傳說中的 Virtual DOM。

每當講到 template 和 JSX,可能就會討論到一個比較大的問題,React 和 Vue 哪個好。一些人可能就不太喜歡通過 JavaScript 直接來表示 UI,然而也會有相當一部分人會認為用 template 來寫可能比較煩,特別是 React 資深玩家。由于 vue 是全球最友好的 UI 框架,有廣大的群眾基礎,一些群眾習慣于直接用 HTML 和 CSS 來干代碼,對他們來說,把寫 UI 的邏輯從 HTML 轉到 template ,比讓他們的思路完全變更到開始思考如何用 JavaScript 來構建 UI 要簡單得多。但是也不得不承認,對于一些之前是搞后端的同學, 或者 iOS 和 Android 的開發者來說,之前沒有怎么接觸過 HTML 的,通過字符串模板的方式來編寫 UI 也不太行。

不同用戶的口味不太一樣,蘿卜白菜,各有所愛。就像我這張 PPT,有些人看了可能很興奮,一些人可能覺得我是個傻 X。你可以說一堆模板怎么怎么不好的例子,他也同樣也給 JSX 一頓噴,誰也說服不了誰。所以 Vue 干脆把兩個事都干了。
4. 什么是「真正的」JSX

JSX[2] 最早是由 facebook 起草的一個規范,后面的這個 X 可以理解為它是 JavaScript 的語法擴展,感興趣的同學可以從這個鏈接進去看看里面的具體內容。由于各個前端框架的實現不一樣,所以它不會由引擎或瀏覽器實現,需要 Transform 之后轉成常規的 JS 之后,這一步操作我們可以理解為「賦能」,才能在瀏覽器里面運行。JSX 其實也和模板語言類似,但它具有 JavaScript 的全部功能,但是由于在模板中的一些限制,用模板寫出來的代碼性能要比 JSX 好得多。
<h1>Hello,?world!</h1>;
這里的 JSX 語法編譯之后其實就是:
import?{?createVNode?as?_createVNode?}?from?"vue"_createVNode("h1",?null,?"Hello,?world!");
5. Vue 3 帶來的改變
Vue 2 早期是用純 JavaScript 來編寫的,隨著項目越來越龐大,引入了 Facebook 的 Flow[3]。雖然 Flow 在一定程度上起到了幫助作用,但還是存在一些問題,尤大也曾經公開表示當初沒有選擇 TypeScript 選擇了 Flow 是「押錯寶」了。

在 Vue 2 中,JSX 的編譯需要依賴 @vue/babel-preset-jsx 和 @vue/babel-helper-vue-jsx-merge-props 這兩個包。前面這個包來負責編譯 JSX 的語法,后面的包用來引入運行時的 mergeProps 函數。

但是如果你要用 TSX 的環境來寫,還需要額外安裝 vue-tsx-support[4]。

在 Vue 3 中,只要安裝一個 Babel 插件就完事了,可以理解為不再需要額外的第三方庫,源碼中就有 jsx.d.ts[5] 用來支持 JSX 的類型檢查

6. 使用 JSX 的場景
我們現在來看下有哪些場景用 JSX 會比模板更加優雅。
6.1 一個文件寫多個組件
一個 .vue 文件里面只能寫一個組件,這個說實話在一些場景下還是不太方便,很多時候我們寫一個頁面的時候其實可能會需要把一些小的節點片段拆分到小組件里面進行復用,這些小組件其實寫個簡單的函數組件就能搞定了。如果你現在沒有這個習慣可能就是因為 SFC 的限制讓你習慣了全部寫在一個文件里面。

比如這里我們封裝了一個 Input 組件,我們希望同時導出 Password 組件和 Textarea 組件來方便用戶根據實際需求使用,而這兩個組件本身內部就是用的 Input 組件,只是定制了一些 props。在 JSX 里面就很方便,寫個簡單的函數組件基本上就夠用了,通過 interface 來聲明 props 就好了。但是如果是用模板來寫,可能就要給拆成三個文件,或許還要再加一個 index.js 的入口文件來導出三個組件,摸魚的時間又少了。
6.2 強依賴編譯時的檢查

模板中引用了一個未在 script 中聲明的 a,vscode 插件可以幫忙檢查出來,但是仍然可以跑起來。

如果是用 TS 來寫,這里引用了一個未聲明的 c 變量,TS 在編譯階段就能讓代碼直接跑不起來。目前模板還是會被直接編譯成 JS,因此還做不到在 template 就進行編譯時的類型檢查。
擁有 JS 完全編程能力

由于 JSX 的本質就是 JavaScript,所以它具有 JavaScript 的完全編程能力。舉個例子,我們需要通過一段邏輯來對一組 DOM 節點做一次 reverse,如果在模板里面寫,那估計要寫兩段代碼。雖然這個例子可能不太常見,但是不得不否認,在一些場景下,JSX 還是要比模板寫起來更加順手。
6.3 范型組件

在模板里面,由于一些歷史的原因,目前范型組件確實還支持不了,但是不代表以后不行。如果非要用范型,可以先用函數組件給包一層,但是注意不要聲明 FunctionalComponent 的類型。這里我們在 .tsx 文件里面聲明 Foo 組件,Props 是一個范型。聲明完之后,再回到模板里面,可以我們看到,剛剛定義的范型組件已經生效了。SFC 的 TS IDE 支持可以用 volar。volar 還支持了范型組件,用起來感覺和 TSX 已經沒多大區別了。
7. 使用 JSX 需要注意的點
7.1 對 Props 的處理
在模板中,對 props 的處理是 merge。為了滿足不同用戶的需求,開了一個可以覆蓋的口子。
7.2 對插槽的處理

插槽是一種內容分發(content distribution)的 API,洋文叫 Slot,也就是 createVNode 的最后一個參數。適合用在結果比較復雜,組件內容可以復用的地方,簡單來說就是在組件中可以預留空間,從父級把內容給傳進去。在 JSX 中,父組件給子組件來傳遞 VNode 通過屬性來傳遞就完事了。
但是在模板中,傳遞屬性的時候,template 里面是不能寫 VNode 的,因此 Vue 里出現了插槽這個概念,插槽只在組件的 children 里面才有。我們來看下 Vue 是怎么處理插槽的:

Vue 對插槽的要求最好是一個 function,對運行時的性能提升會有很大的幫助。因此 A 組件的子節點會被編譯成,{ default: () => [123] }。對應到 JSX 中,按照正常用戶的心智模式,只有一個 children 的時候,寫成{ default: () => [123] }也不太現實,正常的寫法就是直接塞一個 children。但是在編譯階段要處理成 function,否則在開發時會報 warning,對開發者來說是非常不友好的體驗。對編譯器來說其實也好辦,給子節點的 VNode 包一層函數就完事了。

在多個插槽的情況下,稍微比單個的場景要復雜點。除了 default 之外的插槽,通過 props 的方式來傳是不可能的,只能想辦法通過類似「指令」的方式來傳遞,因此最早設計了 v-slots 的命令來處理插槽。但是 v-slots 對于一些開發者來說可能會不夠直觀。更直觀的方式應該像這樣,也就是 obejct slots:

先簡單講一下兩個概念:編譯和運行時。編譯就是把我們的代碼轉成 JavaScript 引擎可以看懂的代碼,運行時就是 JavaScript 引擎開始跑你的代碼。就好比我們招聘中的簡歷篩選和面試,簡歷篩選可以對應編譯,面試來運行時。這個候選人到底怎么樣,單純看簡歷是看不出來的。再回到剛剛的問題,如果直接把 children 寫成一個內聯的對象還好辦,但如果是一個變量的話,在編譯的時候,編譯器是無法知道傳過來的到底是個什么玩意兒,是 slots 還是 VNode 其實編譯的時候看不出來。如果是一個文件里面的,編譯器或許還能判斷,但是從另一個文件 import 進來,是無法判斷的。Babel 處理每一個的文件都是一個「閉環」 。所以這時候就需要加一個運行時的判斷:

雖然解決了判斷是不是 slots 的問題,但是每一個變量給加上運行時判斷,會對編譯產物的體積有一些影響。jsx-next #255

為了保持編譯產物體積和直觀語義上的平衡,就讓開發自己來選擇是否需要上述的 feature,提供了 enableObjectSlots 的開關。
8. 模板與 JSX 的性能對比

剛剛說了一些在哪些場景下用 JSX 可能會更加地合適。這里簡單地對比了下實現相同功能,JSX 和模板的性能差異。左右兩個 demo 里面,整了兩萬個節點,奇數節點里面 class 是動態的,偶數節點的 textContent 是動態的,點擊 shuffle。在這個例子里面,用模板寫的代碼 比用 JSX 寫的要快十幾毫秒。在實際的場景中,組件的層級嵌套遠比這里給出的 demo 要復雜,這個時候就更加能夠體現模板的優勢了。
在傳統的 VDOM 樹中,我們在運行時不能夠得到用于優化的信息。在 Vue 3 中,充分利用了模板靜態信息,最終體現到 VDOM 樹上。比方說在 diff 的時候,可以知道哪些節點是動態的,節點的哪些屬性是動態的。有了這些信息我們就可以在創建 VNode 的時候來標記哪些屬性是不是動態的(靶向更新),也就是傳說中 PatchFlags。除了 PatchFlags 之外,Vue 3 的 VDOM 在運行時,還做了一些緩存,比如 children 的緩存。

先來解釋一下 PatchFlags 是怎么運作的,其實它就是一個數字,只不過在運行的時候被賦予了不同的含義:
數字 2 (PatchFlags.CLASS):表示 class 是動態的
數字 4 (PatchFlags.STYLE):表示 style 是動態的
可能一些同學不太明白這樣來表示有啥好處 CLASS = 1 << 1
,這其實就是用二進制來表示,在上面的代碼中:
TEXT?=?0000000001CLASS?=?0000000010STYLE?=?0000000100
比如一個節點的 class 和 style 都是動態的,就給標記上 PatchFlags.CLASS | PatchFlags.STYLE,得到 0000000011 。想要判斷它的 TEXT 是不是動態的,只需要 FLAG & TEXT > 0 就行。這么看起來只要把 props 的屬性做標記好像 JSX 里面也能對 VDOM 做標記了?

我們來看稍微復雜點的場景。我們看到 textarea 依賴了 attrs,所以編譯完對應的 PatchFlag 應該是
_createVNode("textarea",?_mergeProps({"id":?"textarea"
},?attrs),?null,?16);

單獨把這段代碼拿出來跑是沒問題的,但是由于 textarea 的外層還套了一些組件,attrs 是單獨定義的一個變量,并不是響應式的。我們先不管 attrs 這個變量,把這段代碼當做是模板里面的。在模板編譯的時候,A 的 children 在編譯的時候其實做了一層緩存,每次重新渲染的時候,不需要再去創建 children 的 VNODE,同時對于 children 來說,形成了一個閉包。如果這段代碼編譯的時候,把 children 做了緩存,會打上一個靜態的標記,那么 attrs 拿到永遠是第一次渲染的值。
<A>{{default:?()?=>?(//?children)}}
</A>
所以當點擊 +1s 的時候,并不會觸發視圖的更新。這個時候只能放棄組件 A 的優化,children 不做緩存。因此一旦在某個子節點傳入了一個非響應式的變量,它的所有父節點的 children 就要放棄緩存,因此在每次 re-render 的時候都會重新創建,優化并不是很明顯。然而上面這種寫法在 JSX 中還挺常見的。
除了 PatchFlags 之外,Vue 里有一個叫 SlotFlags 概念,來處理 children 的不同情況。上面的情況,需要把 children 標記為 DYNAMIC,來放棄對 children 的緩存。因此如果你用 JSX 來寫 Vue 的話,基本上是享受不到 Vue 3 對模板做的優化。
9. 總結

參考資料
[1]
Babel JSX Plugin: https://github.com/vuejs/jsx-next
[2]JSX: https://facebook.github.io/jsx/
[3]Flow: https://flow.org/
[4]vue-tsx-support: https://github.com/wonderful-panda/vue-tsx-support
[5]jsx.d.ts: https://github.com/vuejs/vue-next/blob/master/packages/runtime-dom/types/jsx.d.ts
最近組建了一個江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你進群。
·················?若川出品?·················
今日話題
昨天發了一篇原創文章,取消關注7人。發什么都有人取消關注,我已經習慣了。交流群里有這樣的消息。
看到不滿意的文章就是隨手一個取消關注。
看到轉載太多的公眾號就是隨手一個取消關注。
看到廣告太多的公眾號就是隨手一個取消關注。
公眾號號主確實不易。不信的話你可以試著運營一個。
歡迎在下方留言~? 歡迎分享、收藏、點贊、在看我的公眾號文章~
一個愿景是幫助5年內前端人走向前列的公眾號
可加我個人微信?ruochuan12,長期交流學習
推薦閱讀
我在阿里招前端,我該怎么幫你?(現在還能加我進模擬面試群)
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
點擊上方卡片關注我、加個星標
學習源碼整體架構系列、年度總結、JS基礎系列