You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

254 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 13 | JSX如何利用JSX应对更灵活的开发场景
你好,我是大圣。
在上一讲中我给你介绍了如何使用Chrome和Vue Devtools来调试项目相信你已经拥有了调试复杂项目的能力。今天我们来聊一个相对独立的话题就是Vue中的JSX。你肯定会有这样的疑惑JSX不是React的知识点吗怎么Vue里也有
实际上Vue中不仅有JSX而且Vue还借助JSX发挥了Javascript动态化的优势。此外Vue中的JSX在组件库、路由库这类开发场景中也发挥着重要的作用。对你来说学习JSX可以让你实现更灵活的开发需求这一讲我们重点关注一下Vue中的JSX。
## h函数
在聊JSX之前我需要先给你简单介绍一下h函数因为理解了h函数之后你才能更好地理解JSX是什么。下面我会通过一个小圣要实现的需求作为引入来给你讲一下h函数。
在Vue 3的项目开发中template是Vue 3默认的写法。虽然template长得很像HTML但Vue其实会把template解析为render函数之后组件运行的时候通过render函数去返回虚拟DOM你可以在Vue Devtools中看到组件编译之后的结果。
![图片](https://static001.geekbang.org/resource/image/75/af/75e3242df6e45538a6d43c5f0d39a1af.png?wh=1920x1140)
在上面的示意图中,调试窗口右侧代码中的\_sfc\_render\_函数就是清单应用的template解析成JavaScript之后的结果。所以除了template之外在某些场景下我们可以直接写render函数来实现组件。
先举个小例子我给小圣模拟了这样一个需求我们需要通过一个值的范围在数字1到6之间的变量去渲染标题组件 h1~h6并根据传递的props去渲染标签名。对于这个需求小圣有点拿不准了不知道怎么实现会更合适于是小圣按照之前学习的template语法写了很多的v-if
```xml
<h1 v-if="num==1">{{title}}</h1>
<h2 v-if="num==2">{{title}}</h2>
<h3 v-if="num==3">{{title}}</h3>
<h4 v-if="num==4">{{title}}</h4>
<h5 v-if="num==5">{{title}}</h5>
<h6 v-if="num==6">{{title}}</h6>
```
从上面的代码中你应该能感觉到小圣这样的实现看起来太冗余。所以这里我教你一个新的实现方法那就是Vue 3中的[h函数](https://v3.cn.vuejs.org/api/global-api.html#h)。
由于render函数可以直接返回虚拟DOM因而我们就不再需要template。我们在src/components目录下新建一个文件Heading.jsx 要注意的是这里Heading的结尾从.vue变成了jsx。
在下面的代码中, 我们使用defineComponent定义一个组件组件内部配置了props和setup。这里的setup函数返回值是一个函数就是我们所说的render函数。render函数返回h函数的执行结果h函数的第一个参数就是标签名我们可以很方便地使用字符串拼接的方式实现和上面代码一样的需求。像这种连标签名都需要动态处理的场景就需要通过手写h函数来实现**。**
```javascript
import { defineComponent, h } from 'vue'
export default defineComponent({
props: {
level: {
type: Number,
required: true
}
},
setup(props, { slots }) {
return () => h(
'h' + props.level, // 标签名
{}, // prop 或 attribute
slots.default() // 子节点
)
}
})
```
然后在文件src/About.vue中我们使用下面代码中的import语法来引入Heading之后使用level传递标签的级别。这样之后在浏览器里访问 [http://localhost:9094/#/about](http://localhost:9094/#/about) 时就可以直接看到Heading组件渲染到浏览器之后的结果。
```xml
<template>
<Heading :level="3">hello geekbang</Heading>
</template>
<script setup>
import Heading from './components/Head.jsx'
</script>
```
上面的代码经过渲染后的结果如下:
## ![图片](https://static001.geekbang.org/resource/image/7a/e8/7a4d4901c4cc483977d6a423aa4e29e8.png?wh=1120x440)
手写的h函数可以处理动态性更高的场景。**但是如果是复杂的场景h函数写起来就显得非常繁琐需要自己把所有的属性都转变成对象**。并且组件嵌套的时候对象也会变得非常复杂。不过因为h函数也是返回虚拟DOM的所以有没有更方便的方式去写h函数呢答案是肯定的这个方式就是JSX。
## JSX是什么
我们先来了解一下JSX是什么JSX来源自React框架下面这段代码就是JSX的语法我们给变量title赋值了一个h1标签。
```javascript
const element = <h1 id="app">Hello, Geekbang!</h1>
```
**这种在JavaScript里面写HTML的语法就叫做JSX**算是对JavaScript语法的一个扩展。上面的代码直接在JavaScript环境中运行时会报错。JSX的本质就是下面代码的语法糖h函数内部也是调用createVnode来返回虚拟DOM。在之后的课程中对于那些创建虚拟DOM的函数我们统一称为h函数。
```javascript
const element = createVnode('h1',{id:"app"}, 'hello Geekbakg')
```
在从JSX到createVNode函数的转化过程中我们需要安装一个JSX插件。在项目的根目录下打开命令行执行下面的代码来安装插件
```bash
npm install @vitejs/plugin-vue-jsx -D
```
插件安装完成后我们进入根目录下打开vite.config.js文件去修改vite配置。在vite.config.js文件中我们加入下面的代码。这样在加载JSX插件后 现在的页面中就可以支持JSX插件了。
```javascript
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [vue(),vueJsx()]
})
```
然后我们进入src/componentns/Heading.jsx中把setup函数的返回函数改成下面代码中所示的内容这里我们使用变量tag计算出标签类型直接使用渲染使用一个大括号把默认插槽包起来就可以了。
```javascript
setup(props, { slots }) {
const tag = 'h'+props.level
return () => <tag>{slots.default()}</tag>
}
```
我们再来聊一下JSX的语法在实战中的要点详细的要点其实在[GitHub文档](https://github.com/vuejs/jsx-next/blob/dev/packages/babel-plugin-jsx/README-zh_CN.md)中也有全面的介绍,我在这里主要针对之前的清单应用讲解一下。
我们进入到src/components下面新建文件Todo.jsx在下面的代码中我们使用JSX实现了一个简单版本的清单应用。我们首先使用defineComponent的方式来定义组件在setup返回的JSX中使用vModel取代v-model并且使用单个大括号包裹的形式传入变量title.value 然后使用onClick取代@click。循环渲染清单的时候使用.map映射取代v-for使用三元表达式取代v-if。
```javascript
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup(props) {
let title = ref('')
let todos = ref([{ title: "学习 Vue 3", done: true },{ title: "睡觉", done: false }]);
function addTodo(){
todos.value.push({
title:title.value
})
title.value = ''
}
return () => <div>
<input type="text" vModel={title.value} />
<button onClick={addTodo}>click</button>
<ul>
{
todos.value.length ? todos.value.map(todo=>{
return <li>{todo.title}</li>
}): <li>no data</li>
}
</ul>
</div>
}
})
```
通过这个例子,你应该能够认识到,**使用JSX的本质还是在写JavaScript**。在Element3组件库设计中我们也有很多组件需要用到JSX比如时间轴Timeline、分页Pagination、表格Table等等。
就像在TimeLine组件的[源码](https://github.com/hug-sun/element3/blob/master/packages/element3/packages/timeline/Timeline.vue#L35)中有一个reverse的属性来决定是否倒序渲染我们在下面写出了类似的代码。代码中的Timeline是一个数组数组中的两个元素都是JSX我们可以通过数组的reverse方法直接进行数组反转实现逆序渲染。类似这种动态性要求很高的场景template是较难实现的。
```javascript
export const Timeline = (props)=>{
const timeline = [
<div class="start">8.21 开始自由职业</div>,
<div class="online">10.18 专栏上线</div>
]
if(props.reverse){
timeline.reverse()
}
return <div>{timeline}</div>
}
```
## JSX和Template
看到这里你一定会有一个疑惑我们该怎么选择JSX和template呢接下来我就和你聊聊template和JSX的区别这样你在加深对template的理解的同时也能让你逐步了解到JSX的重要性。
先举个例子我们在极客时间官网购买课程的时候就如下图所示的样子页面顶部有搜索框、页面左侧有课程的一些类别。我们按照极客时间对课程的分类比如前端、后端、AI、运维等分类可以很轻松地筛选出我们所需类别的课程。
试想一下,如果没有这些条件限制,而是直接显示课程列表,那你就需要自己在几百门的课程列表里搜索到自己需要的内容。也就是说,接受了固定分类的限制,就能降低选择课程的成本。**这就告诉我们一个朴实无华的道理:我们接受一些操作上的限制,但同时也会获得一些系统优化的收益。**
![图片](https://static001.geekbang.org/resource/image/44/a4/4470104541451a1084dd5f17d5fc7ca4.png?wh=1920x918)
在Vue的世界中也是如此template的语法是固定的只有v-if、v-for等等语法。[Vue的官网中](https://v3.cn.vuejs.org/api/directives.html)也列举得很详细也就是说template遇见条件渲染就是要固定的选择用v-if。这就像极客时间官网上课程的分类是有限的我们需要在某一个类别中选择课程一样。我们按照这种固定格式的语法书写这样Vue在编译层面就可以很方便地去做静态标记的优化。
而JSX只是h函数的一个语法糖本质就是JavaScript想实现条件渲染可以用if else也可以用三元表达式还可以用任意合法的JavaScript语法。也就是说**JSX可以支持更动态的需求。而template则因为语法限制原因不能够像JSX那样可以支持更动态的需求**。这是JSX相比于template的一个优势。
**JSX相比于template还有一个优势是可以在一个文件内返回多个组件**我们可以像下面的代码一样在一个文件内返回Button、Input、Timeline等多个组件。
```javascript
export const Button = (props,{slots})=><button {...props}>slots.default()</button>
export const Input = (props)=><input {...props} />
export const Timeline = (props)=>{
...
}
```
在上面我们谈到了JSX相比于template的优势那么template有什么优势呢你可以先看下面的截图这是使用Vue官方的template解析的[一个demo](https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Cdiv%20id%3D%5C%22app%5C%22%3E%5Cn%20%20%20%20%3Cdiv%20%40click%3D%5C%22()%3D%3Econsole.log(xx)%5C%22%20%20name%3D%5C%22hello%5C%22%3E%7B%7Bname%7D%7D%3C%2Fdiv%3E%5Cn%20%20%20%20%3Ch1%20%3E%E6%8A%80%E6%9C%AF%E6%91%B8%E9%B1%BC%3C%2Fh1%3E%5Cn%20%20%20%20%3Cp%20%3Aid%3D%5C%22name%5C%22%20class%3D%5C%22app%5C%22%3E%E6%9E%81%E5%AE%A2%E6%97%B6%E9%97%B4%3C%2Fp%3E%5Cn%3C%2Fdiv%3E%5Cn%22%2C%22ssr%22%3Afalse%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22filename%22%3A%22Foo.vue%22%2C%22prefixIdentifiers%22%3Afalse%2C%22hoistStatic%22%3Atrue%2C%22cacheHandlers%22%3Atrue%2C%22scopeId%22%3Anull%2C%22inline%22%3Afalse%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22compatConfig%22%3A%7B%22MODE%22%3A3%7D%2C%22whitespace%22%3A%22condense%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup-const%22%2C%22setupRef%22%3A%22setup-ref%22%2C%22setupConst%22%3A%22setup-const%22%2C%22setupLet%22%3A%22setup-let%22%2C%22setupMaybeRef%22%3A%22setup-maybe-ref%22%2C%22setupProp%22%3A%22props%22%2C%22vMySetupDir%22%3A%22setup-const%22%7D%2C%22optimizeBindings%22%3Afalse%7D%7D)。
![图片](https://static001.geekbang.org/resource/image/d5/c4/d57a43f06d47e740b17ba996df051ec4.png?wh=1920x769)
在demo页面左侧的template代码中你可以看到代码中的三个标签。页面右侧是template代码编译的结果我们可以看到相比于我们自己去写h函数在template解析的结果中有以下几个性能优化的方面。
首先,静态的标签和属性会放在\_hoisted变量中并且放在render函数之外。这样重复执行render的时候代码里的h1这个纯静态的标签就不需要进行额外地计算并且静态标签在虚拟DOM计算的时候会直接越过Diff过程。
然后是@click函数增加了一个cache缓存层这样实现出来的效果也是和静态提升类似尽可能高效地利用缓存。最后是由于在下面代码中的属性里那些带冒号的属性是动态属性因而存在使用一个数字去标记标签的动态情况。
比如在p标签上使用8这个数字标记当前标签时只有props是动态的。而在虚拟DOM计算Diff的过程中可以忽略掉class和文本的计算这也是Vue 3的虚拟DOM能够比Vue 2快的一个重要原因。
```javascript
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("h1", null, "技术摸鱼", -1 /* HOISTED */)
const _hoisted_3 = ["id"]
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_createElementVNode("div", {
onClick: _cache[0] || (_cache[0] = ()=>_ctx.console.log(_ctx.xx)),
name: "hello"
}, _toDisplayString(_ctx.name), 1 /* TEXT */),
_hoisted_2,
_createElementVNode("p", {
id: _ctx.name,
class: "app"
}, "极客时间", 8 /* PROPS */, _hoisted_3)
]))
}
// Check the console for the AST
```
在template和JSX这两者的选择问题上只是选择框架时角度不同而已。**我们实现业务需求的时候也是优先使用template动态性要求较高的组件使用JSX实现**尽可能地利用Vue本身的性能优化。
在课程最后的生态源码篇中我们还会聊到框架的设计思路那时你就会发现除了template和JSX之外一个框架的诞生还需要很多维度的考量比如是重编译还是重运行时等等学到那里的时候你会对Vue有一个更加深刻的理解。
## 总结
今天这一讲的主要内容就讲完了我们来简单总结一下今天学到了什么吧。今天我主要带你学习了Vue 3中的JSX。首先我们学习了h函数简单来说h函数内部执行createVNode并返回虚拟DOM而JSX最终也是解析为createVnode执行。而在一些动态性要求很高的场景下很难用template优雅地实现所以我们需要JSX实现。
因为render函数内部都是JavaScript代码所以render函数相比于template会更加灵活但是h函数手写起来非常的痛苦有太多的配置所以我们就需要JSX去方便快捷地书写render函数。
JSX的语法来源于React在Vue 3中会直接解析成h函数执行所以JSX就拥有了JS全部的动态性。
最后我们对比了JSX和template的优缺点template由于语法固定可以在编译层面做的优化较多比如静态标记就真正做到了按需更新而JSX由于动态性太强只能在有限的场景下做优化虽然性能不如template好但在某些动态性要求较高的场景下JSX成了标配这也是诸多组件库会使用JSX的主要原因。
## 思考题
在你现在实现的需求里有哪些是需要JSX的呢
欢迎在留言区分享你的看法,也欢迎你把这一讲推荐给你的同事和朋友们,我们下一讲再见。