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

前言

我平常工做都是使用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插件

爲了能讓上面那段代碼跑起來,還須要有一個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會將傳入的屬性作一個分類,會分爲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.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自動展開,使得refreactive在使用上沒有太大區別。

後話

我我的對Vue 3.0是很是滿意的,不管是對TS的支持,仍是新的Composition API,若是不限制框架的話,那Vue之後確定是個人首選。

個人文章都會最早發佈在個人GitHub博客上,歡迎關注

相關文章
相關標籤/搜索