你可能并不了解JSX

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


这一系列的React原理部分,我总结起来是非常困难的,因为会去综合各种资料,然后阅读,归纳,总结,最后再把自己的理解总结成一篇文章,因为对于React原理部分目前我也处于学习阶段,往往学着学着发现有一个东西没听过,卡住了,然后会把这篇文章暂放,又去学习新的东西,不过好在随着我逐渐深入了解,这些知识慢慢的串成了一条线,逐渐清晰起来。

React技术的更新迭代是非常迅速的,由于中文这边文档比较欠缺,目前很多介绍React的一些文章可能有部分内容已经是过时信息,而本篇文章着重就是梳理一下我在最近准备面试的过程中,对React进行深入学习,我发现我之前在写项目的过程中,虽然天天写JSX,但却从来没有深入理解过,为什么JSX能够被正确的渲染

JSX基础

其实React官方在深入 JSX这篇文章中,对于JSX的讲解非常详细,我这里会挑选一些重点重新梳理一遍,如果大家有时间,我还是推荐读一下官方文档。

JSX仅仅只是一个语法糖,其实它会被转换成一个方法:React.createElement(component, props, ...children)

也正是因为JSX最终都会被编译为React.createElement(component, props, ...children)这种形式的代码,所以React(React 17之前)必须要在自定义组件中进行引入,比如下面的代码:

// 需要引入 React
import React from 'react';

function Test() {
  return <div />;
}

点语法

使用点语法来引用一个React组件其实也是十分常见的,比如Antd中的<Form.Item />组件,还有React文档中提供的下面的例子:

import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  },
};

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

自定义组件

以小写字母开头的元素代表一个 HTML 内置组件,比如div,span,因为在转换的过程中,小写开头的元素会被转换成字符串传入React.createElement,比如”div”,”span”。

所以用户自定义的组件必须以大写开头!如 <Foo /> 会编译为 React.createElement(Foo)。而不会将它转换为字符串。

如果你非要以小写开头,那么得将它赋值给一个大写字母开头的变量。

import React from "react";

// 组件应该以大写字母开头,如果非要以小写开头,那么在使用之前需要赋值给一个大写开头的变量
function hello(props) {
  return <div>Hello {props.toWhat}</div>;
}

export default function HelloWorld() {
  // 赋值给一个大写开头的变量
  const Hello = hello;
  return <Hello toWhat="World"/>;
}

动态组件

如果要根据某个变量来决定组件的动态渲染,如下面的情况,那么你得先使用一个大写字母开头的变量去接收:

import React, { useState } from 'react';

const components = {
  photo: () => <div>显示照片</div>,
  video: () => <div>显示视频</div>,
};

export default function Story() {
  const [storyType, setStoryType] = useState<'photo' | 'video'>('photo');
  const SpecificStory: () => JSX.Element = components[storyType];

  return (
    <div>
      <SpecificStory/>
      <button
        onClick={() => {
          setStoryType('video');
        }}
      >
        切换
      </button>
    </div>
  );
}

效果:

change

条件渲染

React的文档中对于这一部分也介绍的非常详细,具体可以参考条件渲染,这里我只介绍我经常使用到的这一部分。

这里谈及几个常用的,官方的条件渲染这一章讲的更加的详细:

与运算符&&

这个形式的条件渲染我在写项目的时候运用的特别多,这也是我比较推荐的写法,

import React, { useState } from 'react';

export default function TestComponent() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div>
      {isVisible && <div>isVisible为true的时候才渲染</div>}
      <button
        onClick={() => {
          setIsVisible(!isVisible);
        }}
      >
        切换
      </button>
    </div>
  );
}

最终的渲染结果:

isVisible

三元表达式

import React, { useState } from "react";

export default function TestComponent() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div>
      {isVisible ? <div>isVisible为true的时候才渲染</div> : <div>isVisible为false的时候才渲染</div>}
      <button
        onClick={() => {
          setIsVisible(!isVisible);
        }}
      >
        切换
      </button>
    </div>
  );
}

具体的渲染如下:

isVisible三元

某些文档或者某些项目上面会以三元表达式来替代与运算符 &&,两者都可以达到同样的效果,但是我个人觉得使用三元表达式来替代&&会增加代码阅读时的复杂性。

import React, { useState } from 'react';

export default function TestComponent() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div>
      {
        // 这里用三元表达式可以实现&&同样的效果
        isVisible ? <div>isVisible为true的时候才渲染</div> : null
      }
      <button
        onClick={() => {
          setIsVisible(!isVisible);
        }}
      >
        切换
      </button>
    </div>
  );
}

if语句

if语句在做一些比较复杂的交互时特别有用,比如一个界面某个变量为特定值的时候,会渲染不同的元素。

import React, { useState } from 'react';

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

  return (
    <div>
      <IfTestComponent count={count} />
      <div>当前count值:{count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        切换
      </button>
    </div>
  );
}

function IfTestComponent({ count }: { count: number }) {
  if (count === 1) {
    return <div>这是count等于1时的渲染</div>;
  }

  if (count === 2) {
    return <div>这是count等于2时的渲染</div>;
  }

  if (count === 3) {
    return <div>这是count等于3时的渲染</div>;
  }

  // 其它情况下就不再进行渲染
  return null;
}

最终的渲染结果如下:

countChange

循环

关于循环的内容有点多,可以参考一下官方的列表 & Key这篇文章,我在之前做项目中,手动进行列表遍历渲染的情况并不多,一般这种数据都是直接放入Antd的Table组件中,以表格的形式展示出来。

我这里主要讲解一下循环的用法:

import React from "react";

const data = [
  { id: 1, name: '张三', age: 18, sex: '男' },
  { id: 2, name: '李四', age: 19, sex: '男' },
  { id: 3, name: '王五', age: 17, sex: '女' },
];

export default function TestComponent() {
  return (
    <div>
      {data.map((item) => (
        <div>
          姓名:{item.name},年龄:{item.age},性别:{item.sex}
        </div>
      ))}
    </div>
  );
}

其实你打开浏览器的控制台,你会发现一个警告:

image-20220322143638978

根据我的经验来说,只要你使用了map,那么你需要在map返回的元素中绑定key值:

<div>
  {data.map((item) => (
    // 绑定key值
    <div key={item.id}>
      姓名:{item.name},年龄:{item.age},性别:{item.sex}
    </div>
  ))}
</div>

这个时候警告就没了,可能你有时候不想在外面嵌套一层div,那么你可以使用空标签<Fragment />

export default function TestComponent() {
  return (
    <div>
      {data.map((item) => (
        // 绑定key值
        <React.Fragment key={item.id}>
          姓名:{item.name},年龄:{item.age},性别:{item.sex}
        </React.Fragment>
      ))}
    </div>
  );
}

Props

在JSX中,Props的处理是非常灵活的,也正是因为灵活的Props,让React在创建一些复杂组件的时候就会显得异常的方便。

字符串字面量

当如果你要传入的prop是一个字符串时,你就可以省略括号,如下所示:

<MyComponent message="hello world" />

<MyComponent message={'hello world'} />

React官方说这种行为是不重要的,随便用哪一种都可以,但是我个人习惯在prop为字符串的时候省略掉括号。

Props的默认值

当如果不给prop赋值,那么它的默认值为true,也就是说下面的两种写法是等价的。

<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />

官方表示不建议不传递 value 给 prop…然而我非常喜欢当为true时不传值给prop。

属性展开

我特别喜欢React的可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象,更妙的是,使用ES6的语法可以只将部分属性传递下去。

import React from 'react';

export interface ButtonProps {
  kind: string;
  onClick?: () => void;
  children?: React.ReactNode;
}

const Button = (props: ButtonProps) => {
  // 这里可以使用对象的解构,将kind属性保留下来,剩余的其它属性传入button中
  const { kind, ...other } = props;
  const className = kind === 'primary' ? 'PrimaryButton' : 'SecondaryButton';
  return <button className={className} {...other} />;
};

export default function TestComponent() {
  return (
    <div>
      <Button kind="primary" onClick={() => console.log('clicked!')}>
        Hello World!
      </Button>
    </div>
  );
}

该特性在对一些UI组件进行二次封装上特别的好用,并且配合TypeScript,可以获得非常完美的组件使用提示。

children

布尔类型、Null 以及 Undefined 将会忽略,但是0依然会被渲染

import React from 'react';

export default function TestComponent() {
  return (
    <div>
      {0}
      {undefined}
      {null}
      {false}
      {true}
    </div>
  );
}

最终渲染结果:

image-20220322145017918

所以警惕下面这种代码,可能会引起不必要的BUG

import React from "react";

const list = [];

export default function TestComponent() {
  return (
    <div>
      {list.length && <div>这里不会渲染</div>}
    </div>
  );
}

渲染结果和上面的图一样,为0

子元素

相信React子元素的用法我们在平时写项目中已经非常熟悉了,但是还有一种形式我们用的比较少,比如下面这种返回一个数组的形式:

数组

需要注意的是,如果返回的是存储在数组中的一组元素,那么还的给这些元素设置key值,不然会抛出异常。

import React from 'react';

export default function TestComponent() {
  // 不需要用额外的元素包裹列表元素!
  return [
    // 不要忘记设置 key :)
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}

渲染结果:

image-20220322150127488

函数

下面这种用法其实我在项目中没有使用过,但是官方上有提到,所以这里直接放出官方的代码:

import React from 'react';

// 调用子元素回调 numTimes 次,来重复生成组件
function Repeat(props: {
  numTimes: number;
  children: (index: number) => React.ReactNode;
}) {
  let items = [];
  for (let i = 0; i < props.numTimes; i++) {
    items.push(props.children(i));
  }
  return <div>{items}</div>;
}

export default function TestComponent() {
  return (
    <Repeat numTimes={10}>
      {(index) => <div key={index}>This is item {index} in the list</div>}
    </Repeat>
  );
}

渲染结果:

image-20220322150347777

React.createElement

当然,你也可以直接使用React.createElement进行创建子元素,我们随便找两个例子来改一改,因为我觉得实际编写项目的过程中,应该不会有人直接使用React.createElement这个方法。

import React from 'react';

export default function TestComponent() {
  return React.createElement(
    'h1',
    null,
    '这是使用React.createElement创建的元素',
  );
}

渲染结果:

image-20220322150759766

到目前为止,上面的内容可能是大多数文章中都会提到的,JSX仅仅只是一个语法糖,其实它会被转换成一个方法:React.createElement,但我想说,下面的内容才是你在面试中可以和面试官battle的东西。

$$typeof

不管是写的JSX,还是使用的React.createElement,我们打印一下它的属性,会发现有下面的几个值:

image-20220322151029343

前面的几个属性type、props、key、ref可能就算不知道它的具体意思,但是大概意思应该还是会懂,但是这个$$typeof完全猜不到它的含义,为什么它的值是Symbol.for('react.element')

你多多少少的可能听过XSS攻击,其实这个$$typeof值就是为了防止网站受到XSS攻击。

其实$$typeof: REACT_ELEMENT_TYPE标记了该对象是个React Element。

因为JSON中是不支持 Symbol 类型,React 会检测 element.$$typeof,如果元素丢失或者无效,会拒绝处理该元素。

这里就得提到React提供的另一个全局API React.isValidElement

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

可以发现,React在验证一个元素是不是合法元素时,会去验证$$typeof这个值,如果

具体详情可以看一下这篇文章:为什么React元素有一个$$typeof属性?

React 17

React17的发布并没有包含什么新特性,但是它提供了一个全新的JSX转换,意思就是JSX 不会再被转换为 React.createElement

  • 使用全新的转换,你可以单独使用 JSX 而无需引入 React
  • 根据你的配置,JSX 的编译输出可能会略微改善 bundle 的大小
  • 它将减少你需要学习 React 概念的数量,以备未来之需。

至于这个第三点…在面试的时候可能就是需要问这些概念上的问题,所以即使你知道从React17开始,JSX就不再被编译成React.createElement了,但你还是得清楚曾经JSX曾经就是React.createElement的语法糖。

那么新的JSX转换器又会将JSX语句编译成什么呢?

如下所示:

function App() {
  return <h1>Hello World</h1>;
}

编译结果:

// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello world' });
}

根据文档中所说:jsx这个函数的属性即参数如下:

function jsx(type, props, key) {
  return {
    $$typeof: ReactElementSymbol,
    type,
    key,
    props,
  };
}

与React.createElement最大的不同是,现在children需要放在第二个参数,也就是props中。

至于这部分内容,React官方在介绍全新的 JSX 转换中有详细的讲解。

深入JSX

到上面为止都属于JSX的基础部分,而JSX更加深入的东西在Fiber架构下的diff算法部分,这里就先提一下,关于diff算法的部分,我也会逐渐开始总结。

JSX节点,最终会通过一系列的处理,变成一个Fiber节点。

关于React如何通过JSX创建出Fiber节点的,可以看一下createElement源码

注意:源码中if (__DEV__) {}的那部分都是在开发环境才会进入的语句,所以关于这部分我们可以不去理会它。

export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // 验证config中的ref是否合法
    // return config.ref !== undefined;
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    // 验证config中的key是否合法
    // return config.key !== undefined;
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 将剩余的属性添加到一个新的 props 对象中
    for (propName in config) {

      // 验证是否有保留属性字段
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  /** children的数量 */
  const childrenLength = arguments.length - 2;
  // 数量为 1 则children为对象 否则为数组
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 处理class组件中的defaultProps属性
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 传入到 ReactElement 中进行处理 返回最终处理完毕的对象
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

在createElement中,最终调用了ReactElement这个方法,那么我们看一下该方法的源码:

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // React通过这个属性来判断是否是一个React元素
    // const REACT_ELEMENT_TYPE = Symbol.for('react.element');
    $$typeof: REACT_ELEMENT_TYPE,

    // 元素内置的属性
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 记录了谁创建了该组件
    _owner: owner,
  };

  return element;
};

我们再来看一个方法isValidElement,该方法用来验证对象是否是一个合法的React元素:

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

跟上面说的一致,$$typeof就是React用来判断该对象是否是一个合法的React元素。

JSX和Fiber

最后

JSX的使用时非常灵活的,我曾经在另一篇文章中写了我在项目中会用到的JSX骚操作,如果有兴趣的话也可以去看一下:迷人的JSX,你可以进行各种骚操作

其实本篇文章讲的JSX也属于一些基础,在Fiber节点中还有大量和JSX相关的内容,我争取这几天一步一步的将它们进行吃透。