前言
我日常工作都是使用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'; interface InputProps { value: string; onChange: (value: string) => void; } const Input = defineComponent({ setup(props: InputProps) { const handleChange = (event: KeyboardEvent) => { props.onChange(event.target.value); } return () => ( <input value={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插件轉換為下面這行代碼:
const input = 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會將傳入的屬性做一個分類,會分為class、style、attrs、on等不同部分。這樣做非常繁瑣,也不好處理。
在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方法。
電腦刺綉綉花廠 http://www.szhdn.com 廣州品牌設計公司https://www.houdianzi.com
實際使用
在上面的工作完成以后,我們可以真正開始做開發了。
渲染子節點
上文說到,子節點不會像React那樣作為children這個prop傳遞,而是要通過slots去取:
例如實現一個Button組件
// button.tsx import { defineComponent } from 'vue'; import './style.less'; interface ButtonProps { type: 'primary' | 'dashed' | 'link' } const Button = defineComponent({ setup(props: ButtonProps, { slots }) { return () => ( <button class={'btn', `btn-${props.type}`}> {slots.default()} </button> ) } }) export default Button;
然后我們就可以使用它了:
import { createApp } from 'vue'; import Button from './button'; // vue 3.0也支持函數組件 const App = () => <Button>Click Me!</Button> createApp().mount(App, '#app');
渲染結果:
Reactive
配合vue 3.0提供的reactive,不需要主動通知Vue更新視圖,直接更新數據即可。
例如一個點擊計數的組件Counter:
import { defineComponent, reactive } from 'vue'; const Counter = defineComponent({ setup() { const state = reactive({ count: 0 }); const handleClick = () => state.count++; return () => ( <button onClick={handleClick}> count: {state.count} </button> ) } });
這個Counter組件如果用React Hooks來寫:
import React, { useState } from 'react'; const Counter = () => { const [count, setCount] = useState(0); const handleClick = () => setCount(count + 1); return ( <button onClick={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'; const App = defineComponent({ setup() { const count = ref(0); const handleClick = () => { count.value ++; } return () => ( <div > <button onClick={handleClick}>toggle</button> <Transition name="slide-fade"> {withDirectives(<h1>Count: {count.value}</h1>, [[ vShow, count.value % 2 === 0 ]])} </Transition> </div> ) } })
這樣寫起來有點繁瑣,應該可以通過babel-jsx插件來實現下面這種寫法:
<h1 vShow={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() {
return h('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自動展開,使得ref和reactive在使用上沒有太大區別。