发布于

vue中使用jsx

Authors
  • avatar
    Name
    田中原
    Twitter

Vue JSX 简介

template

在 Vue 里, sfc(single file component) 是一个以 .vue 结尾的文件,通常包含三种类型的顶级语言块 <template><script><style>,可以理解为 HTML 、JS 以及 CSS 的组合。每一个 .vue 文件结尾的文件都是一个组件,而且只能 export default 出一个组件。SFC 的具体定义是单文件组件,它本身就是把一个文件看作一个单位,所以他的约束性是要大很多的,你必须具有固定的文件结构才能使用 SFC,这做了很多的限制:

  • 一个文件只能写一个组件
  • 节点片段只能写在 template 里面,有点不灵活
  • 变量绑定只能获取this上面的内容,不能使用全局变量(很多时候我们都要把全局变量先挂载到this上)

JSX 是什么

官方是这么定义它的:JSX 是一个 JavaScript 的语法扩展,但它具有 JavaScript 的全部功能。

JSX最早是由 facebook 起草的一个规范,由于各个前端框架的实现不一样,所以它不会由引擎或浏览器实现,需要进行 Transform ,我们将使用 Babel 之类的转置器将JSX转换成常规的 JS ,才能在浏览器里面运行。

基本上,JSX 允许我们在 JS 中使用类似 Html 的语法:

JSX 其实就是方法调用,他和 JS 是有一对一对应关系的,我们来看jsx例子:

// JSX的示例
const el = <h1>Hello World</h1>

这里的 JSX 语法编译之后其实就是:

// 这里的 JSX 语法编译之后其实就是:
import { createVNode as _createVNode } from 'vue'
_createVNode('h1', null, 'Hello, world!')

el节点其实就是返回VNode,打印结果如下,也是一个标准化子节点的结构和createVNode创建的是一样的:

image-JSX

在 Vue 中无论使用 Template 还是 JSX 都会被编译成 h 函数(在 Vue 2 中称为 render 函数)。在 Vue 中使用 JSX,只是换了一种表现形式而已。Vue 知道如何将此虚拟节点挂载到 DOM 上,它会更新我们在浏览器中看到的内容,但是 VNode 从哪里来的呢?实际还有一个步骤,Vue 基于我们的 Template 创建一个渲染函数,返回一个虚拟 DOM 节点。

image-JSX

Template 转为 Render Function 的编译过程:第一步通过html-parser将template解析成ast抽象语法树,第二步通过optimize优化ast并标记静态节点和静态根节点,第三步通过generate将ast抽象语法树编译成render字符串并将静态部分放到staticRenderFns中,最后通过new Function(render)生成render函数,这些 render function 在运行时阶段,就是 Virtual DOM

以前有小伙伴分享过虚拟dom形成可参考之前文档。

所以,了解这个过程,您可能不是那么排斥在 Vue 中使用 JSX 了,或许对它在 Vue 中的表现稍微不那么陌生了。

而 JSX 就是这些了,没有什么更多的内容,所以说 JSX 只是方便我们写嵌套的函数调用的语法糖,而其本身没有扩展任何其他的内容。

但是 SFC 就不一样了,SFC 定义的不仅是语法,更是文件。

Vue3 对 JSX 的过渡

在 Vue 2 中,JSX 的编译需要依赖 @vue/babel-preset-JSX@vue/babel-helper-vue-JSX-merge-props 这两个包。前面这个包来负责编译 JSX 的语法,后面的包用来引入运行时的 mergeProps 函数。

但是如果你要用 TSX 的环境来写,还需要额外安装 vue-tsx-support。但在 Vue 3 中,只要安装一个 Babel 插件就可以使用了。可以理解为不再需要额外的第三方库,不需要自己再去补充添加jsx声明文件,源码中就有用来支持 JSX 的类型检查可参考 jsx.d.ts 声明文件,也就是以下代码为 Vue 添加 TypeScript JSX 声明,确保TypeScript 可以加载声明文件。

declare global {
  namespace JSX {
    interface Element extends VNode {}
    interface ElementClass {
      $props: {}
    }
    interface ElementAttributesProperty {
      $props: {}
    }
    interface IntrinsicElements extends NativeElements {
      // allow arbitrary elements
      // @ts-ignore suppress ts:2374 = Duplicate string index signature.
      [name: string]: any
    }
    interface IntrinsicAttributes extends ReservedProps {}
  }
}

为什么在 Vue 里也支持 JSX

有时候,我们使用渲染函数(render function)来抽象组件,而渲染函数有时候写起来是非常痛苦的,当我们在 Vue 中定义 html 模板时,Vue 的模板编译器将其编译为一个createElement函数。

// HTML
<div>
  <h1>Hello World</h1>
</div>

// 模板编译器将把上面的html转换成:
render (createElement) {
  return createElement(
    'div',
    {},
    createElement(
      'h1',
      {},
      'Hello World'
    )
  )
}

一旦定义了具有许多元素嵌套级别或具有多个同级元素的组件,我们就会遇到新问题,书写复杂的 render 函数异常痛苦,可读性很差并且难以维护。

这就是 JSX 出现的原因,它可以很好的解决此类问题。

Vue 官方推荐的开发方式是 template,在大部分场景下,尤其是在业务场景下。虽然 Vue 3 的 template 提供了很多的性能优化,但是对于一些库的开发者来说,template 可能不够灵活,而使用 JSX 的方式就比较灵活。

使用 JSX 的场景

官方提供下面这个例子:

动态生成标题的组件时,组件中就是接收父组件传过来的level值来显示不同的h标签,v-if可以说用到了极致,而且写了很多个冗余的slot。

<template>
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
</template>
<script lang="jsx">
import { defineComponent } from 'vue'
export default defineComponent({
  props: {
    level: Number,
  },
  setup(props, { slots }) {
    // 或者使用JSX来实现。
    const tag = `h${props.level}`
    return () => <tag>{slots.default()}</tag>
  },
})
</script>

使用JSX来实现,省去了很多冗余代码,页面一下清爽了很多。

JSX灵活性

一个 SFC文件里面只能写一个组件,这个说实话在一些场景下还是不太方便,很多时候我们写一个页面的时候其实可能会需要把一些小的节点片段拆分到小组件里面进行复用,这些小组件其实写个简单的函数组件就能搞定了。

例如像一个文件写多个组件:

const Input = (props) => <input {...props} />

export default Input

export const Textarea = (props) => <Input {...props} />

export const Password = (props) => <Input type="password" {...props} />

比如这里我们封装了一个 Input 组件,我们希望同时导出 Password 组件和 Textarea 组件来方便用户根据实际需求使用,而这两个组件本身内部就是用的 Input 组件,只是定制了一些 props。在 JSX 里面就很方便,写个简单的函数组件基本上就够用了。但是如果是用模板来写,可能就要给拆成三个文件,或许还要再加一个 index.js 的入口文件来导出三个组件。此时使用 JSX 可以很灵活地控制动态 DOM 片段。

变量作用域和强依赖编译时的检查

  • Template中无法使用当前作用域变量,必须return后才能使用
  • JSX 中可以直接使用当前作用域的变量
image-JSX

模板中引用了一个未在 script 中声明的 a,vscode 插件可以帮忙检查出来,但是代码仍然可以跑起来。

image-JSX

如果是用 TS 来写,这里引用了一个未声明的 c 变量,TS 在编译阶段就能让代码直接跑不起来。目前模板还是会被直接编译成 JS,因此还做不到在 template 模版中就进行编译时的类型检查。

拥有 JS 完全编程能力,一切可以变量化。

假设有一个场景,组件中需要根据 props 上的 reverse 属性,来决定是否要调换两块内容的渲染顺序。

如果通过模板来实现,在不抽象子组件的情况下,foo 和 bar 的模板结构需要重复写两遍,才能满足这个需求:

<template>
  <div>
    <template v-if="reverse">
      <div class="bar">Bar DOM...</div>
      <div class="foo">Foo DOM...</div>
    </template>
    <template v-else>
      <div class="foo">Foo DOM...</div>
      <div class="bar">Bar DOM...</div>
    </template>
  </div>
</template>

在 JSX 中可以很容易实现:

const renderContent = () => {
  const Content = [<div class="foo">Foo DOM...</div>, <div class="bar">Bar DOM...</div>]
  if (props.reverse) {
    Content.reverse()
  }
  return <div>{Content}</div>
}

JSX 其实也和模板语言类似,但它具有 JavaScript 的全部功能,但是由于在模板中的一些限制,用模板写出来的代码性能要比 JSX 好得多。

Vue 3 中对 JSX 带来的改变

  • 属性传递

    Vue 2 中,仅仅属性就有三种:组件属性 props,普通 html 属性 attrs,DOM 属性 domProps。

    Vue 3 中,属性这块的传递和 React 类似,意味这不需要再传递 props,attrs 这些属性。

// before
{
  class: ['foo', 'bar'],
  style: { color: 'red' },
  attrs: { id: 'foo' },
  domProps: { innerHTML: '' },
  on: { click: foo },
  key: 'foo'
}

// after
{
  class: ['foo', 'bar'],
  style: { color: 'red' },
  id: 'foo',
  innerHTML: '',
  onClick: foo,
  key: 'foo'
}
  • 指令改版

Vue 3 把大多数全局 API 和 内部 helper 移到了 ES 模块中导出(譬如 v-model、transition、teleport),从而使得 Vue 3 在增加了很多新特性之后,基线的体积反而小了。

v-modelv-show 这些 API 全部通过模块导出的方式来引入。

基线体积: 无法舍弃的代码的体积

我们来看一段非常简单的代码 <input v-model="x" />,在 Vue 2 和 Vue 3 中的编译结果有何不同

// before
function render() {
  with (this) {
    return _c('input', {
      directives: [
        {
          name: 'model',
          rawName: 'v-model',
          value: x,
          expression: 'x',
        },
      ],
      domProps: {
        value: x,
      },
      on: {
        input: function ($event) {
          if ($event.target.composing) return
          x = $event.target.value
        },
      },
    })
  }
}
// after
import {
  vModelText as _vModelText,
  createVNode as _createVNode,
  withDirectives as _withDirectives,
  openBlock as _openBlock,
  createBlock as _createBlock,
} from 'vue'

export function render(_ctx, _cache) {
  return _withDirectives(
    (_openBlock(),
    _createBlock(
      'input',
      {
        'onUpdate:modelValue': ($event) => (_ctx.x = $event),
      },
      null,
      8 /* PROPS */,
      ['onUpdate:modelValue']
    )),
    [[_vModelText, _ctx.x]]
  )
}

可以看到在 Vue 3 中,对各个 API 做了更加细致的拆分,理想状态下,用户可以在构建时利用摇树优化 (tree-shaking) 去掉框架中不需要的特性,只保留自己用到的特性。模版编译器会生成适合做 tree-shaking 的代码,不需要使用者去关心如何去做,这部分的改动同样需要在 JSX 写法中实现。

模板编译器中增加了 PatchFlag,在 JSX 的编译过程同样也做了处理,性能会有提升,但是考虑到 JSX 的灵活性,做了一些兼容处理,该功能还在测试阶段。

使用 JSX 需要注意的点

Fragment

在vue3的模版语法中是支持解析多根节点的语法结构的,比如这样:

<template>
  <div></div>
  <div></div>
  <div></div>
</template>

但是使用JSX的方式是不支持这种写法的,还是必须只有一个根结点,这个时候我们可以和react一样通过添加一个虚拟节点来完成同样的需求:

const App = () => (
  <>
    <span>I'm</span>
    <span>Fragment</span>
  </>
)

对 Props 的处理

在模板中,对 合并 class / style / onXXX handlers 的处理是 默认是merge。jsx也是进行属性合并,为了满足不同用户的需求,开了一个可以覆盖的口子。

enableObjectSlots

使用 enableObjectSlots 。虽然在 JSX 中比较好使,但是会增加一些 _isSlot 的运行时条件判断,这会增加你的项目体积。即使你关闭了 enableObjectSlotsv-slots 还是可以使用。

对插槽的处理

jsx 中,应该使用 v-slots 代替 v-slot

插槽是 createVNode 的最后一个参数。适合用在结果比较复杂,组件内容可以复用的地方,简单来说就是在组件中可以预留空间,从父级把内容给传进去。在 JSX 中,父组件给子组件来传递 VNode 通过属性来传递就完事了。

还有很多用法注意到可以参考jsx-next

我们平时开发还是多用temlate因为直观简洁,各种指令用着很方便,等你觉得用template写出的代码看着很冗余,或者想自己控制渲染逻辑比如循环,判断等等时可以考虑用JSX。

模板与 JSX 的性能对比

这里偶然在vue conf看到vue3 JSXPlugin开发和维护者在分享上说了一个场景下的性能对比的一个场景,这里简单地对比了下实现相同功能,JSX 和模板的性能差异。

image-JSX

左右两个 demo 里面,整了两万个节点,奇数节点里面 class 是动态的,偶数节点的 textContent 是动态的,点击 shuffle。在这个例子里面,用模板写的代码 比用 JSX 写的要快十几毫秒。在实际的场景中,组件的层级嵌套远比这里给出的 demo 要复杂,这个时候就更加能够体现模板的优势了。

在传统的 VDOM 树中,我们在运行时不能够得到用于优化的信息。在 Vue 3 中,充分利用了模板静态信息,最终体现到 VDOM 树上。比方说在 diff 的时候,可以知道哪些节点是动态的,节点的哪些属性是动态的。有了这些信息我们就可以在创建 VNode 的时候来标记哪些属性是不是动态的(靶向更新),也就是传说中 PatchFlags。除了 PatchFlags 之外,Vue 3 的 VDOM 在运行时,还做了一些缓存。

SFC与 JSX 的比较

  1. JSX并没有更加方便,只是让你更接近vue模板渲染的底层原理。JSX书写会比较强迫的让你把for循环等小逻辑模板单独成为一个函数,优点就是颗粒度更小了,和sfc书写方式相比,没有那么方便。在vue中JSX可以作为一种优化方式(官方推荐使用template方式意味着,template方式bug会优先解决),而不是大规模使用。JSX导入组件,不再需要注册所需的每个组件,自定义组件更容易导入和管理。
  2. SFC 享受不到 props 类型提示,组件在外部使用时 vscode 无法做出 props 的类型提示,这一点对于组件库来讲是个痛点如果是用 tsx 编写的组件,用户是可以享受到 props 的类型提示的。还是比较推荐defineComponent方式,在 vue3 中无论是否使用 TS,通过 defineComponent 定义组件都能获得更好的提示。
  3. 指令比较:
    • SFC 原生支持优雅的指令写法。
    • JSX本身对指令的书写方式支持不友好。
  4. 运行时性能比较
    • SFC 支持hoist,block,patchProps等运行时性能提升,至少比 JSX性能快了3倍
    • JSX 没有运行时优化
  5. 生态比较
    • 当前知名的UI库比如 ant-desing-vue,vant 内部采用了 JSX;但对外提供的组件示例代码仍然是 vue的模板语法。当前 Vue 组件库文档示例代码几乎没有可能从 Template 向 JSX 过渡,或者向用户提供 JSX 的组件示例代码。如果您的项目依赖组件库,对于使用者如果要用 JSX,还得自己把 vue 改成 JSX,比较麻烦,这无疑增加开发成本。

参考资料

JSX

jsx-next

jsx.d.ts

vue-tsx-support