zoukankan      html  css  js  c++  java
  • 使用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都有一些不同。

    例如这样一段代码:

    <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在使用上没有太大区别。

  • 相关阅读:
    CF821E 【Okabe and El Psy Kongroo】
    BZOJ1231: [Usaco2008 Nov]mixup2 混乱的奶牛
    P1896 [SCOI2005]互不侵犯
    QBXT Day 2 记录
    CF467C George and Job
    【luogu P3373 线段树2】 模板
    【luogu P1306 斐波那契公约数】 题解
    【luogu T24743 [愚人节题目5]永世隔绝的理想乡】 题解
    【luogu P1903 [国家集训队]数颜色】 题解
    莫队算法~讲解
  • 原文地址:https://www.cnblogs.com/xiaonian8/p/13825892.html
Copyright © 2011-2022 走看看