迷人的JSX,你可以进行各种骚操作

您好,我是沧沧凉凉,是一名前端开发者,目前在掘金知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。


本篇文章是我在去年6月份就准备写的,但那个时候仅仅使用React做了一个多月的项目,所以对很多JSX的使用方法并没有那么得心应手,后面经过长达半年的使用,我发现JSX能够做到的事情太多了,以及JSX创建一些复用性组件上也比Vue模板语法具有更大的优势。

事先声明:Vue和React各有各的优势,本篇文章有些地方是为了对比而不是踩一捧一。

1. 创建可复用组件

能创建可复用组件没有什么稀奇的,现代前端框架都可以创建可复用组件,但在JSX中你创建的组件只要你愿意,组件中的所有内容都可以很方便的进行修改。

在我从Vue切换到React时创建组件的时候去React官方文档上面找过一个Vue有的功能:插槽

在Vue中只要你创建组件,那么大部分情况都是需要使用到插槽的,但是我在React文档中找了老半天,貌似React中没有这个概念,当时我还感觉到很奇怪,因为没有这个东西那么创建组件的时候复用性就没有那么高。

经过我的仔细研究,我发现React中根本不需要有插槽,因为JSX语法带来了一种更加强大的写法,任何Props属性都可以作为插槽。

思考一个场景,如果你要更改自定义组件上的图标,或者将文字改成按钮,那你可以考虑下在Vue中怎么进行实现,没错,几乎最好的方式就是使用插槽,那如果你要改变底部的两个按钮的名字呢,使用Vue会如何进行实现?那如果你不想要这两个按钮,想自定义底部,或者根本都不需要底部,那么你使用Vue应该怎么实现呢?

上面的一系列问题是不是让你感到非常的头疼,如果有更多需要自定义的地方,你是不是要在Vue中大量使用插槽,甚至动态插槽?而在JSX中可以很简单的解决这些问题。

我们来看一下下面的这个例子,你可以想象一下如果是使用Vue的话如何进行创建此类型的组件。

在JSX中,没有插槽这个概念,但它具有比插槽更好用的特性,那就是组件上面的所有属性都可以作为插槽,插入ReactNode,比起插槽来讲,JSX的这种方式简便直观,比如下面的这段代码。

import React from "react";

interface TestProps {
  children: React.ReactNode;
  buttonRender?: React.ReactNode;
}

function Test({ buttonRender, children }: TestProps) {
  return (
    <>
      {children}
      {buttonRender}
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <Test buttonRender={<button>这里可以传入dom</button>}>测试</Test>
    </div>
  );
}

最终渲染结果如下:

image-20220104100518071

可不要小看这个功能,在创建自定义组件的时候是非常实用的,它可以给使用这个组件的用户极大的定制权,比如我们改一下上面的代码:

import React from "react";

interface TestProps {
  children: React.ReactNode;
  buttonRender?: React.ReactNode;
}

function Test({ buttonRender, children }: TestProps) {
  return (
    <>
      {children}
      {buttonRender || <button>这里默认的按钮</button>}
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <Test>测试</Test>
    </div>
  );
}

渲染结果:

image-20220104100723405

总的来说,你可以给一个默认的ReactNode,如果用户不传入属性,那么就显示这个默认的ReactNode,如果一旦传入属性值,则替换默认的ReactNode。

但是这里需要强调的是,跟Vue的插槽一样,这种通过属性渲染的ReactNode,是没有办法读取子节点上的属性的,会直接报错

image-20220104101614112

总之,React的任何Props属性都可以作为插槽进行使用,这给组件创建带来了极强的可定制性,如果你有使用过Antd,你会发现Antd这个组件库有非常多的地方都可以让你进行定制。

2. 使用变量及函数

在很多时候,我们想要转换一些页面上的值,如果是在Vue模板语法中你要使用第三方库中提供的函数,那么你需要在data中进行声明,才能够在模板语法中使用 。我们以monentjs中为例,就要下面这么操作:

<template>
  <div id="app">
    {{ moment().format("h:mm:ss") }}
  </div>
</template>

<script>
import moment from "moment";

export default {
  name: "App",
  data() {
    return {
      // 这里需要导出才能在界面上使用
      moment,
    };
  },
};
</script>

而在JSX中,你可以直接引用第三方库或者你在其它地方定义的函数,我们还是以monentjs为例,你可以直接在JSX中进行使用:

import moment from "moment";

export default function App() {
  return <div className="App">{moment().format("h:mm:ss")}</div>;
}

在模板语法中使用第三方库会比JSX多出一步导出步奏,而且由于Vue中你声明在data属性中是会自动进行双向绑定的,可能会导致一些性能上的问题或其它意料之外的BUG,当然你也可以使用Vue高级语法取消双向绑定,甚至冻结该属性,就是操作起来麻烦一点。

3. 任意提取ReactNode

JSX最为灵活的地方就是你可以将任意的ReactNode写在函数中,直接在JSX中进行使用,这样的好处在于如果你提取成新的组件,那么就涉及到传值的问题,但是你提取成函数,可以直接引用当前函数中已经声明的变量,可能表达起来比较麻烦,所以我们直接看一下下面的例子:

import { useState } from "react";

export default function App() {
  const [title, setTitle] = useState("Hello world!");

  // 这里可以提取ReactNode
  const show = <h1>{title}</h1>;

  return (
    <div className="App">
      {/* 直接使用 */}
      {show}
      <button
        onClick={() => {
          setTitle("同样可以改变");
        }}
      >
        点我
      </button>
    </div>
  );
}

上面的代码最终的效果就如下图所示:

change

JSX具有的这项特性,可以创建非常多的骚操作,同时它可以配合useMemo进行使用,我在书写项目的时候,有时候也会使用到这种写法,比如下面的情况

import { useMemo, useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);

  const show = useMemo(() => {
    if (count === 0) return <div>Hello World!</div>;

    return (
      <img
        src="https://pic.qqtn.com/up/2019-9/15690311636958128.jpg"
        alt="图片"
      />
    );
  }, [count]);

  return (
    <div className="App">
      {show}
      <button
        onClick={() => {
          setCount(1);
        }}
      >
        点我
      </button>
    </div>
  );
}

上面的代码渲染出来就是下面的样子:

change1

4. 递归组件

使用JSX最大的好处之一,递归组件。在Vue模板语法中几乎很难写出递归组件,不过使用Vue也并不是无法写出递归组件。

至于递归组件,我在很早的时候就已经写过一篇对应的文章:React中创建递归组件,所以就不在这里进行赘述了,在项目中导航栏往往会以递归组件的形式来进行编写。

5. 解锁JS完整体

在Vue中,如果你使用了模板语法,会受限于Vue的版本,而无法在模板语法中使用JS的一些新语法,比如在Vue2中无法使用?.这个非常有用的语法,但是在JSX中,只要你的babel够新,那么JSX就支持所有的JavaScript语法,比如下面这种语法:

我要大力推荐?.它的中文名字叫做可选链操作符ES11中,它被正式的纳入了规范,我们先来看下面的代码:

const data = {
  name: "张三",
  age: 18,
  sex: "男"
};

console.log(data.friend.name); // 报错:Uncaught TypeError: Cannot read property 'name' of undefined

所以这个时候我们平时就要做很多判断。

const data = {
  name: "张三",
  age: 18,
  sex: "男"
};

console.log(data && data.friend && data.friend.name); // undefined

而有了可选链操作符(?.)后,我们可以直接简写成:

const data = {
  name: "张三",
  age: 18,
  sex: "男"
};

console.log(data?.friend?.name);

可以发现,代码简化了非常多。

你可能会想,这没什么了不起的,其实这个功能非常的有用,尤其是当数据是从后端获取的情况下,因为后端获取的数据,你完全不知道后端会不会返回undefined,如果一旦返回的是undefined,前端在进行调用的时候,Vue可能会出现界面白屏,而React可能会出现界面报错。

我们将上面的代码放在Vue中使用:

<template>
  <div>这里是测试:{{ data?.friend?.name }}</div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      data: {
        name: "张三",
        age: 18,
        sex: "男",
      },
    };
  },
};
</script>

注意!上面的代码在Vue2中会报语法错误:

image-20220107231755156

而在Vue3中可以正常的解析:

image-20220107231919927

这里额外提一句,Vue3最好还是使用新语法,因为Composition API类似于React的Hooks,体验非常好!

<template>
  <div>这里是测试:{{ data?.name }}</div>
</template>

<script>
import { reactive } from "vue";
export default {
  name: "App",
  setup() {
    const data = reactive({ name: "张三", age: 18, sex: "男" });
    return {
      data,
    };
  },
};
</script>

我们将上面的代码放在React中使用:

const data = { name: "张三", age: 18, sex: "男" };

export default function App() {
  return <div>这里是测试:{data?.friend?.name}</div>;
}

image-20220107232550641

6. 完美搭配TS

在Vue中,因为有模板语法的存在,所以Vue对于TypeScript的支持是一个比较大的问题,比如声明在data中的属性,你就无法很好的给他类型,虽然官方为了解决这个问题,推出了vue-class-component然后又有人在其上进行了一次封装发布了vue-property-decorator,但依然无法完美解决TypeScript和Vue不搭配的问题。

而JSX可以完美结合TypeScript成为TSX,TSX拥有了更多方便维护的特性,比如泛型组件,可以参考这篇文章:TS与React的完美融合:泛型组件

而在React中,这些问题都不复存在,React中的JSX可以完美的配合TypeScript,获得TypeScript的所有能力。

还是那个可选链的问题,我们来看看JSX和TSX中的区别。

JSX中有提示但不报错:

image-20220104175442892

TSX中有语法提示并且有错误信息:

image-20220104175314588

7. 最后

当然,React还提供了一些高级用法,虽然在平时的业务代码中可能并不会用到,但是一旦你要进行创建组件,可能会经常用到,因为我个人写组件的情况非常少,但是在Antd的源码中,大量的使用到了这些高级语法来进行创建组件,在之后我可能会深入去了解这些用法。

一旦你用熟JSX后,你就会发现JSX书写起来实在是太美妙了,仿佛再复杂的组件创建起来你也会很有信息,所以无论你是否使用React,JSX都推荐学习,而且现在TypeScript已经内置了JSX解析。

而且就算你使用Vue,你也可以通过对应的bable使用在Vue中使用JSX语法来创建比较复杂的组件。