我平常工做都是使用React來作開發,可是我對React一直不是很滿意,特別是在推出React Hooks之後。html
不能否認React Hooks極大地方便了開發者,可是它又有很是多反直覺的地方,讓我難以接受。因此在很長一段時間,我都在嘗試尋找React的替代品,我嘗試過很多別的前端框架,但都有各類各樣的問題或限制。前端
在看到了Vue 3.0 Composition-API的設計,確實有眼前一亮的感受,它既保留了React Hooks的優勢,又沒有反覆聲明銷燬的問題,而Vue一直都是支持JSX語法的,3.0對TypeScript的支持又很是好,因此我開始嘗試用Vue + TSX來作開發。vue
Vue 3.0已經發布了alpha版本,能夠經過如下命令來安裝:react
npm install vue@next --save
複製代碼
先來看看用Vue3.0 + TSX寫一個組件是什麼什麼樣子的。git
實現一個Input組件:github
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
這個準備階段完成,最後返回一個「函數組件」。npm
這算是解決了React Hooks很是大的一個痛點,比React Hooks那種重複聲明的方式要舒服多了。api
Vue 3.0對TS作了一些加強,不須要像之前那樣必須聲明props
,而是能夠經過TS類型聲明來完成。數組
這裏的defineComponent
沒有太多實際用途,主要是爲了實現讓ts類型提示變得友好一點。bash
爲了能讓上面那段代碼跑起來,還須要有一個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
去取,這個下文會作說明。因此只能本身動手來實現這個插件,我是在babel-plugin-transform-react-jsx的基礎上修改的,而且自動注入了h
方法。
在上面的工做完成之後,咱們能夠真正開始作開發了。
上文說到,子節點不會像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');
複製代碼
渲染結果:
配合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
在使用上沒有太大區別。
我我的對Vue 3.0是很是滿意的,不管是對TS的支持,仍是新的Composition API,若是不限制框架的話,那Vue之後確定是個人首選。
個人文章都會最早發佈在個人GitHub博客上,歡迎關注