优秀的编程知识分享平台

网站首页 > 技术文章 正文

实践Vue 3.0做JSX(TSX)风格的组件开发

nanyue 2024-07-25 06:00:13 技术文章 19 ℃


作者:莫夭

转发链接:https://zhuanlan.zhihu.com/p/102668383

前言

我日常工作都是使用React来做开发,但是我对React一直不是很满意,特别是在推出React Hooks以后。

前面文章小编也详细整理了React.js和Vue.js的语法:

一篇文章教你并列比较React.js和Vue.js的语法【实践】

不可否认React Hooks极大地方便了开发者,但是它又有非常多反直觉的地方,让我难以接受。所以在很长一段时间,我都在尝试寻找React的替代品,我尝试过不少别的前端框架,但都有各种各样的问题或限制。

更多关于React和Vue 学习实践项目相关文章,请见本篇文章底部

在看到了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方法。

实际使用

在上面的工作完成以后,我们可以真正开始做开发了。

渲染的节点

上文说到,子节点不会像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在使用上没有太大区别。

后话

我个人对Vue 3.0是非常满意的,无论是对TS的支持,还是新的Composition API,如果不限制框架的话,那Vue以后肯定是我的首选。

推荐React和Vue学习资料文章:

一篇文章教你并列比较React.js和Vue.js的语法【实践】

手拉手带你开启Vue3世界的鬼斧神工【实践】

深入浅出通过vue-cli3构建一个SSR应用程序【实践】

怎样为你的 Vue.js 单页应用提速

聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总

【新消息】Vue 3.0 Beta 版本发布,你还学的动么?

Vue真是太好了 壹万多字的Vue知识点 超详细!

Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5

深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】

手把手教你深入浅出vue-cli3升级vue-cli4的方法

Vue 3.0 Beta 和React 开发者分别杠上了

手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件

Vue3 尝鲜

总结Vue组件的通信

手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】

Vue 开源项目 TOP45

2020 年,Vue 受欢迎程度是否会超过 React?

尤雨溪:Vue 3.0的设计原则

使用vue实现HTML页面生成图片

实现全栈收银系统(Node+Vue)(上)

实现全栈收银系统(Node+Vue)(下)

vue引入原生高德地图

Vue合理配置WebSocket并实现群聊

多年vue项目实战经验汇总

vue之将echart封装为组件

基于 Vue 的两层吸顶踩坑总结

Vue插件总结【前端开发必备】

Vue 开发必须知道的 36 个技巧【近1W字】

构建大型 Vue.js 项目的10条建议

深入理解vue中的slot与slot-scope

手把手教你Vue解析pdf(base64)转图片【实践】

使用vue+node搭建前端异常监控系统

推荐 8 个漂亮的 vue.js 进度条组件

基于Vue实现拖拽升级(九宫格拖拽)

手摸手,带你用vue撸后台 系列二(登录权限篇)

手摸手,带你用vue撸后台 系列三(实战篇)

前端框架用vue还是react?清晰对比两者差异

Vue组件间通信几种方式,你用哪种?【实践】

浅析 React / Vue 跨端渲染原理与实现

10个Vue开发技巧助力成为更好的工程师

手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】

1W字长文+多图,带你了解vue的双向数据绑定源码实现

深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现

手把手教你D3.js 实现数据可视化极速上手到Vue应用

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】

Vue3.0权限管理实现流程【实践】

后台管理系统,前端Vue根据角色动态设置菜单栏和路由

React

React 函数式组件性能优化知识点指南汇总

13个精选的React JS框架

深入浅出画图讲解React Diff原理【实践】

【React深入】React事件机制

Vue 3.0 Beta 和React 开发者分别杠上了

手把手深入Redux react-redux中间件设计及原理(上)【实践】

手把手深入Redux react-redux中间件设计及原理(下)【实践】

前端框架用vue还是react?清晰对比两者差异

为了学好 React Hooks, 我解析了 Vue Composition API

【React 高级进阶】探索 store 设计、从零实现 react-redux

写React Hooks前必读

深入浅出掌握React 与 React Native这两个框架

可靠React组件设计的7个准则之SRP

React Router v6 新特性及迁移指南

用React Hooks做一个搜索栏

你需要的 React + TypeScript 50 条规范和经验

手把手教你绕开React useEffect的陷阱

浅析 React / Vue 跨端渲染原理与实现

React 开发必须知道的 34 个技巧【近1W字】

三张图详细解说React组件的生命周期

手把手教你深入浅出实现Vue3 & React Hooks新UI Modal弹窗

手把手教你搭建一个React TS 项目模板

全平台(Vue/React/微信小程序)任意角度旋图片裁剪组件

40行代码把Vue3的响应式集成进React做状态管理

手把手教你深入浅出React 迷惑的问题点【完整版】

Tags:

最近发表
标签列表