使用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博客上,欢迎关注

相关文章
相关标签/搜索