谈谈 Vue 模板和 JSX

工具链

从学习曲线角度来讲,结合我个人体会,React 学习路线是比 Vue 陡峭的,这个和 JSX、Template 有关吗?当然有。在 React 中使用 JSX,众所周知, JSX 需要经过 Transform 才能在浏览器中运行。马上就有小伙伴反驳了,Vue 有官方的 Vue-cli, React 使用 create-react-app 初始化项目就好了呀,并不需要比 Vue 多学习其他工具呀。

我们从另一个角度来看这个问题,使用 JSX 还需要熟练 ESM,这是绕不开的,因为一个 React 组件就是一个 js 或 JSX,这可能会给新手带来疑惑,组件是如何链接的?先别着急反驳,接着往下看。

仅仅掌握“三剑客”的新手就可以轻松上手 Vue,这在 Vue 官方文档中有所体现。

image.png

巧妙之处就在与上图中的 “易用”,仅仅学会了 HTML、CSS、JavaScript 就能上手。Vue 的“渐进式”不仅体现在跟随项目的复杂度上的,还有很大程度上适应使用者的水平。这不是空穴来风,当年鄙人就仅学习了这“三剑客”,就用 Vue-cli 初始化并完成了小项目。如今我思考在当时 Vue 是如何帮我避开 ESM 的。答案在于 vue 的“三段式结构”。

<script>
export defalut  {
  // ...
} 
</script>

我的注意点完全在于导出的这个对象之中以及上方的 Template 和下方的 Style,只知道在这个对象里面写一些 Data、Methods 等等。甚至完全没有在意 export defalut 这个关键字。

不仅如此,Vue 的 Template + Options API 基本就规范了处理一些逻辑时的大体形式,这在通过搜索引擎检索一些遇到的问题时,通常能够轻易看懂检索到的代码,甚至可以“依葫芦画瓢”,这对新手无疑是一个优势。相比 JSX, 写法上,Template 虽不能够从“优雅”上胜出,但 JSX 更容易带入个人编码风格,这应该能使大家有所认同,这也就加大了新手的理解难度。

回到问题,那么 Vue 是如何引入组件的呢?作为新手,我只会“依葫芦画瓢”,通过一条 import 语句导入进来,然后在 components 里面将组件注册,这一切都被我自然得误认为是 Vue 的能力。

这不代表使用 Vue 不需要学习 ESM,没错,这是究极必要的,熟悉 ESM 可以更好得组织项目代码。上面我只是在思考当年 Vue 是怎么帮我避开 ESM 的,或者说我作为新手时是如何毫无察觉地避开它的,Template “功不可没”

题外话,Vue 的 Template 可以放到 Script 标签下方,这可能更符合 React 用户的直觉,看尤大在推上分享一些 Demo 时有时会这么写。如果您乐意,也可以这么做,但在多人协作的项目中还要考虑小伙伴的感受。

<script>
export defalut  {
  // ...
} 
</script>

<template>
  
</template>

Vue + JSX = React?

所谓“萝卜白菜,各有所爱”。在 React 中只能使用 JSX,Vue 从 2.0 开始干脆把 JSX 也支持完事了。有人说,在 Vue 中使用 JSX 为什么不直接使用 react? 这个话题就大了,这不仅和项目的技术选型有关,且 Vue 和 React 设计上就有很大差别。在 Vue 中无论使用 Template 还是 JSX 都会被编译成 h 函数(在 Vue 2 中称为 render 函数)。所以,在 Vue 中使用 JSX,只是换了一种表现形式而已。

我们来看一下 Vue 将 Template 渲染到浏览器的过程。

<div>Hello</div>

这个HTML也可以通过一个虚拟节点 VNode 来表示,也就是用 JavaScript 对象的形式来表示。

{
    tag: 'div',
    children: [
        {
            text: 'Hello'
        }
    ]
}

Vue 知道如何将此虚拟节点并挂载到 DOM 上,它会更新我们在浏览器中看到的内容,但是 VNode 从哪里来的呢?实际还有一个步骤,Vue 基于我们的 Template 创建一个渲染函数,返回一个虚拟 DOM 节点。

vue2.png

渲染函数可以是这样的:

render(h) {
    return h('div', 'hello')
}

当组件更改时,Render 函数将重新运行,它将创建另一个虚拟节点。然后发送旧的 VNode 和新的 VNode 到 Vue中进行比较并以最高效的方式在我们的网页上更新。例如下图所示,仅仅更新了文本内容。
vue3.png

题外话,实际 Vue 有三个核心模块:

  1. Reactivity Module 响应式模块
  2. Compiler Module 编译器模块
  3. Renderer Module 渲染模块

响应式模块允许创建 JavaScript 响应对象并可以观察其变化。当使用对象的代码运行时,它们会被跟踪,因此,它们可以在响应对象发生变化后运行。

编译器模块获取 HTML 模板并将它们编译成渲染函数。这可能在运行时在浏览器中发生,但在构建 Vue 项目时更常见。这样浏览器就可以只接收渲染函数。

渲染模块在网页上渲染组件经历三个不同阶段:在渲染阶段,将调用 render 函数,它返回一个虚拟 DOM 节点;在挂载阶段,使用虚拟 DOM 节点并调用 DOM API 来创建网页;在补丁阶段,渲染器将旧的虚拟节点和新的虚拟节点进行比较并只更新变化的部分。

上面是 Vue 将 Template 渲染到浏览器的过程,和 Template 类似,在 Vue 中使用的 JSX 也会转化成 h 函数,眼见为实。

image.png

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

从另一个方面来讲,Vue 中的 JSX 和 React 中的 JSX 在写法上也有差别。在 Vue JSX 中您可以直接使用熟悉的指令(directives),例如 Vue 内置的 v-show 、 v-model 甚至 v-models 和自定义指令,但例如 slots 在 Vue JSX 中或许没有在 React 中那么方便。

“联合”的能力

在 Vue sfc(single file component)中同时编辑 script、template、style,这提供了“联合”的能力,即在一个文件中使 Style 和 Template 共享 Script,帮助开发者更容易实现业务。例如:rfcs-style-variables,该提案虽然还没有最终确定下来,但 Vue 展示了这种能力。基础示例:

<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data:()=> ({
    color: 'red',
    font: {
      size: '2em'
    }
  })
}
</script>

<style>
.text {
  color: v-bind(color);

  /* expressions (wrap in quotes) */
  font-size: v-bind('font.size');
}
</style>

通过 style-variables 写有一个有趣的示例,此处为部分代码。

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent({
    setup() {
        const inputBorderColor = ref('')
        
        const onEngineChange = engine => {
            for (const { name, color } of enginesData) {
                if (name === engine) {
                    inputBorderColor.value = color
                    return
                }
            }
        }
        
        return {
            inputBorderColor,
        }
    },
})
</script>

<style lang="scss">
input {
  &:focus {
    border-color: v-bind(inputBorderColor);
  }
}
</style>

Video_2021-01-03_112916.gif
如果在 Vue 中使用 JSX, 就无法利用这个优势了。题外话,在 React 中类似的 css-in-js 方案是 styled-component。

业务组件库

Vue 几乎所有面向业务构建的生态内容给出的示例代码都是基于 Template 的。我们通常在项目中安装 UI 组件库依赖,组件库中给出的示例代码也是基于 Template 的。当前 Vue 组件库文档示例代码正在由 Options API 向 Composition API 过渡,但几乎没有可能从 Template 向 JSX 过渡,或者向用户提供 JSX 的组件示例代码。这非常重要,如果您的项目依赖组件库,查看文档后还需要将 Template 转到 JSX,这无疑增加开发成本。

TypeScript 支持

在 script 中,vue3 相比 vue2 已经出现质的飞跃。从 Template 来看,Vue 还有待完善,但这个可行的。JSX 一开始也没有类型支持,完全是 TS 给加了一套针对 JSX 的推导机制。

模版和类型推导,表面上看,隔了一层模版语法 + 编译,似乎确实存在 “断层”,但其实里面没您想的差那么远。Vue 的模版是编译成 virtual dom 渲染函数的,生成的 js 跟 React 的渲染函数一样可以类型推导,而模版跟生成的 js 之间是完整的逻辑映射,所以这里其实主要是需要做一些工具链上的衔接,把对生成的 js 分析出来的 intellisense 反馈到 IDE 里的模版上。技术上是完全可行的。

vscode 插件 Volar 已经支持 Template 表达式的类型检查。Vetur 虽然不支持还未定稿的 RFC,也已经支持基于现有 API 的模板类型检查和组件 props 类型检查了。另外,在 vue3 中无论是否使用 TS,通过 defineComponent 定义组件都能获得更好的提示。
image.png
可以预见的是,Vue 从升级 3.0 后,对于 TypeScript 的支持将会越来越好。

适用场景

在大多数场景下(尤其是业务场景)使用 Template 可能是更好的选择, vue3 基于 Template 分析做了大量的优化,而使用 JSX 需要手动做一些优化。

JSX 由于更具灵活性,通常一些组件库的不二之选,例如:Ant Design Vue、Vant、Element Plus(部分使用)。看到这么个例子: 根据 props 上的 reverse 属性,决定是否要调换两块内容的顺序。使用 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>;
}

如果通过模板来实现,在不抽象子组件的情况下,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 会有一定优势。Composition API + JSX 是某些场景下追求极致的选择,相应地需要付出更多开发成本。

参考资料:

[1] //www.zhihu.com/question/436260027/answer/1647182157
[2] //www.zhihu.com/question/310485097/answer/591869966
[3] //www.cnblogs.com/guangzan/p/13358322.html