使用Vue 3.0做JSX(TSX)風格的組件開發

blank

使用Vue 3.0做JSX(TSX)風格的組件開發

前言

我日常工作都是使用React來做開發,但是我對React一直不是很滿意,特別是在推出React Hooks以後。

不可否認React Hooks極大地方便了開發者,但是它又有非常多反直覺的地方,讓我難以接受。所以在很長一段時間,我都在嘗試尋找React的替代品,我嘗試過不少別的前端框架,但都有各種各樣的問題或限制。

在看到了Vue 3.0 Composition-API的設計,確實有眼前一亮的感覺,它既保留了React Hooks的優點,又沒有反复聲明銷毀的問題,而Vue一直都是支持JSX語法的,3.0對TypeScript的支持又非常好,所以我開始嘗試用Vue + TSX來做開發。

Vue 3.0已經發布了alpha版本,可以通過以下命令來安裝:

npm install vue@next --save

簡單範例

先來看看用Vue3.0 + TSX寫一個組件是什麼什麼樣子的。

實現一個Input組件:

import{defineComponent}from'vue';interfaceInputProps{value:string;onChange:(value:string)=>void;}constInput=defineComponent({setup(props:InputProps){consthandleChange=(event:KeyboardEvent)=>{props.onChange(event.target.value);}return()=>(<inputvalue={props.value}onInput={handleChange}/>)}})

可以看到寫法和React非常相似,和React不同的是,一些內部方法,例如handleChange ,不會在每次渲染時重複定義,而是在setup這個準備階段完成,最後返回一個“函數組件”。

這算是解決了React Hooks非常大的一個痛點,比React Hooks那種重複聲明的方式要舒服多了。

Vue 3.0對TS做了一些增強,不需要像以前那樣必須聲明props ,而是可以通過TS類型聲明來完成。

這裡的defineComponent沒有太多實際用途,主要是為了讓TS類型提示變得友好一點。

Babel插件

為了能讓上面那段代碼跑起來,還需要有一個Babel插件來轉換上文中的JSX,Vue 3.0相比2.x有一些變化,不能再使用原來的vue-jsx插件。

我們都知道JSX(TSX)實際上是語法糖,例如在React中,這樣一段代碼:

const input = <input value="text" />

實際上會被babel插件轉換為下面這行代碼:

constinput=React.createElement('input',{value:'text'});

Vue 3.0也提供了一個對應React.createElement的方法h 。但是這個h方法又和vue 2.0以及React都有一些不同。

例如這樣一段代碼:

<div class={['foo', 'bar']} style={{ margin: '10px' }} id="foo" onClick={foo} />

在vue2.0中會轉換成這樣:

h('div', { class: ['foo', 'bar'], style: { margin: '10px' } attrs: { id: 'foo' }, on: { click: foo } })

可以看到vue會將傳入的屬性做一個分類,會分為classstyleattrson等不同部分。這樣做非常繁瑣,也不好處理。

在vue 3.0中跟react更加相似,會轉成這樣:

h('div', { class: ['foo', 'bar'], style: { margin: '10px' } id: 'foo', onClick: foo })

基本上是傳入什麼就是什麼,沒有做額外的處理。

當然和React.createElement相比也有一些區別:

  • 子節點不會作為以children這個名字在props中傳入,而是通過slots去取,這個下文會做說明。
  • 多個子節點是以數組的形式傳入,而不是像React那樣作為分開的參數

所以只能自己動手來實現這個插件,我是在babel-plugin-transform-react-jsx的基礎上修改的,並且自動注入了h方法。

實際使用

在上面的工作完成以後,我們可以真正開始做開發了。

渲染子節點

上文說到,子節點不會像React那樣作為children這個prop傳遞,而是要通過slots去取:

例如實現一個Button組件

// button.tsximport{defineComponent}from'vue';import'./style.less';interfaceButtonProps{type:'primary'|'dashed'|'link'}constButton=defineComponent({setup(props:ButtonProps,{slots}){return()=>(<buttonclass={'btn',`btn-${props.type}`}>{slots.default()}</button>)}})exportdefaultButton;

然後我們就可以使用它了:

import{createApp}from'vue';importButtonfrom'./button';// vue 3.0也支持函数组件
constApp=()=><Button>ClickMe!</Button>createApp().mount(App,'#app');
渲染結果

Reactive

配合vue 3.0提供的reactive ,不需要主動通知Vue更新視圖,直接更新數據即可。

例如一個點擊計數的組件Counter:

import{defineComponent,reactive}from'vue';constCounter=defineComponent({setup() {conststate=reactive({count: 0});consthandleClick=()=>state.count++;return()=>(<buttononClick={handleClick}>count:{state.count}</button>)}});
渲染結果

這個Counter組件如果用React Hooks來寫:

importReact,{useState}from'react';constCounter=()=>{const[count,setCount]=useState(0);consthandleClick=()=>setCount(count+1);return(<buttononClick={handleClick}>count:{count}</button>)}

對比之下可以發現Vue 3.0的優勢:

在React中, useState和定義handleClick的代碼會在每次渲染時都執行,而Vue定義的組件重新渲染時只會執行setup中最後返回的渲染方法,不會重複執行上面的那部分代碼。

而且在Vue中,只需要更新對應的值即可觸發視圖更新,不需要像React那樣調用setCount

當然Vue的這種定義組件的方式也帶來了一些限制setup的參數props是一個reactive對象,不要對它進行解構賦值,使用時要格外注意這一點:

例如實現一個簡單的展示內容的組件:

// 错误範例import { defineComponent, reactive } from 'vue'; interface LabelProps { content: string; } const Label = defineComponent({ setup({ content }: LabelProps) { return () => <span>{content}</span> } })

這樣寫是有問題的,我們在setup的參數中直接對props做了解構賦值,寫成了{ content }這樣在後續外部更新傳入的content時,組件是不會更新的,因為破壞了props的響應機制。以後可以通過eslint之類的工具來避免這種寫法。

正確的寫法是在返回的方法裡再對props做解構賦值:

import { defineComponent, reactive } from 'vue'; interface LabelProps { content: string; } const Label = defineComponent({ setup(props: LabelProps) { return () => { const { content } = props; // 在这里对props做解构赋值return <span>{content}</span>; } } })

生命週期方法

在Vue 3.0中使用生命週期方法也非常簡單,直接將對應的方法import進來即可使用。

import { defineComponent, reactive, onMounted } from 'vue'; interface LabelProps { content: string; } const Label = defineComponent({ setup(props: LabelProps) { onMounted(() => { console.log('mounted!'); }); return () => { const { content } = props; return <span>{content}</span>; } } })

vue 3.0對tree-shaking非常友好,所有API和內置組件都支持tree-shaking。

如果你所有地方都沒有用到onMounted ,支持tree-shaking的打包工具會自動將起去掉,不會打進最後的包裡。

指令和過渡效果

Vue 3.0還提供了一系列組件和方法,來使JSX也能使用模板語法的指令和過渡效果。

使用Transition在顯示/隱藏內容塊時做過渡動畫:

import { defineComponent, ref, Transition } from 'vue'; import './style.less'; const App = defineComponent({ setup() { const count = ref(0); const handleClick = () => { count.value ++; } return () => ( <div> <button onClick={handleClick}>click me!</button> <Transition name="slide-fade"> {count.value % 2 === 0 ? <h1>count: {count.value}</h1> : null} </Transition> </div> ) } })

// style.less .slide-fade-enter-active { transition: all .3s ease; } .slide-fade-leave-active { transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0); } .slide-fade-enter, .slide-fade-leave-to { transform: translateX(10px); opacity: 0; }

渲染結果

也可以通過withDirectives來使用各種指令,例如實現模板語法v-show的效果:

import{defineComponent,ref,Transition,withDirectives,vShow}from'vue';import'./style.less';constApp=defineComponent({setup() {constcount=ref(0);consthandleClick=()=>{count.value++;}return()=>(<div><buttononClick={handleClick}>toggle</button><Transitionname="slide-fade">{withDirectives(<h1>Count:{count.value}</h1>,[[vShow,count.value%2===0]])}</Transition></div>)}})

這樣寫起來有點繁瑣,應該可以通過babel-jsx插件來實現下面這種寫法:

<h1vShow={count.value%2 ===0}>Count: {count.value}</h1>

優缺點

在我看來Vue 3.0 + TSX完全可以作為React的替代,它既保留了React Hooks的優點,又避開了React Hooks的種種問題。

但是這種用法也有一個難以忽視的問題:它沒辦法獲得Vue 3.0編譯階段的優化。

Vue 3.0通過對模板的分析,可以做一些前期優化,而JSX語法是難以做到的。

例如“靜態樹提升”優化:

如下一段模板(這是模板,並非JSX):

<template><div><span>static</span><span>{{ dynamic }}</span></div></template>

如果不做任何優化,那麼編譯後得到的代碼應該是這樣子:

render(){returnh('div',[h('span','static'),h('span',this.dynamic)]);}

那麼每次重新渲染時,都會執行3次h方法,雖然未必會觸發真正的DOM更新,但這也是一部分開銷。

通過觀察,我們知道h('span', 'static')這段代碼傳入的參數始終都不會有變化,它是靜態的,而只有h('span', this.dynamic)這段才會根據dynamic的值變化。

在Vue 3.0中,編譯器會自動分析出這種區別,對於靜態的節點,會自動提升到render方法外部,避免重複執行。

Vue 3.0編譯後的代碼:

const __static1 = h('span', 'static'); render() { return h('div', [ __static1, h('span', this.dynamic) ]) }

這樣每次渲染時就只會執行兩次h 。換言之,經過靜態樹提升後,Vue 3.0渲染成本將只會和動態節點的規模相關,靜態節點將會被復用。

除了靜態樹提升,還有很多別的編譯階段的優化,這些都是JSX語法難以做到的,因為JSX語法本質上還是在寫JS,它沒有任何限制,強行提升它會破壞JS執行的上下文,所以很難做出這種優化(也許配合prepack可以做到)。

考慮到這一點,如果你是在實現一個對性能要求較高的基礎組件庫,那模板語法仍然是首選。

另外JSX也沒辦法做ref自動展開,使得refreactive在使用上沒有太大區別。

後話

我個人對Vue 3.0是非常滿意的,無論是對TS的支持,還是新的Composition API ,如果不限制框架的話,那Vue以後肯定是我的首選。

我的文章最先發表在我的[GitHub部落格]( github.com/hujiulong/bl ),歡迎關注


更新:

本文中通過TS的interface聲明props類型的依賴vue3的Optional props decalration ,但後續版本中這個功能被廢除了,原因可以查看#154 ,在#1155中也有一些替代方案的討論

What do you think?

Written by marketer

blank

2019 前端之路

blank

🧨性能強悍的TS 版G6 來了,給您拜個早年🧨