vue tree组件_使用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';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都有一些不同。
例如這樣一段代碼:
在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方法。
實際使用
在上面的工作完成以后,我們可以真正開始做開發了。
渲染子節點
上文說到,子節點不會像React那樣作為children這個prop傳遞,而是要通過slots去取:
例如實現一個Button組件
// button.tsximport { 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}>togglebutton><Transition name="slide-fade">
{withDirectives(<h1>Count: {count.value}h1>, [[
vShow, count.value % 2 === 0
]])}Transition>div>
)
}
})
這樣寫起來有點繁瑣,應該可以通過babel-jsx插件來實現下面這種寫法:
2 === 0}>Count: {count.value}</h1>
優缺點
在我看來Vue 3.0 + TSX完全可以作為React的替代,它既保留了React Hooks的優點,又避開了React Hooks的種種問題。
但是這種用法也有一個難以忽視的問題:它沒辦法獲得Vue 3.0編譯階段的優化。
Vue 3.0通過對模板的分析,可以做一些前期優化,而JSX語法是難以做到的。
例如“靜態樹提升”優化:
如下一段模板(這是模板,并非JSX):
<div><span>staticspan><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博客,歡迎關注
總結
以上是生活随笔為你收集整理的vue tree组件_使用Vue 3.0做JSX(TSX)风格的组件开发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: adc 采样 校准
- 下一篇: GPS nmealib学习笔记