专栏名称: 厨猿加加
打工人中最会做饭的厨子
今天看啥  ›  专栏  ›  厨猿加加

女友 React 30 问 -- JSX 是什么?

厨猿加加  · 掘金  ·  · 2021-05-01 15:45

文章预览

阅读 85

女友 React 30 问 -- JSX 是什么?

引子

今天有个同学问我,我们在写 React 项目的时候,文件后缀为什么是 .jsx? 我愣了一下,不是 .jsx,也可以是 .tsx 啊,你问这个是啥意思嘞。他接着说,我们写的这些都是 js 文件,那么我直接用 .js 后缀不行吗,为啥非要在文件名后缀上做功夫呢?

JSX 本质是什么,它和 JS 有什么区别

作为一名 React 的用户,日常的工作就是使用 JSX 去描述 React 组件内容。我们都知道 JSX 就是 React 提供的,对用户友好的一种编写组件的语法糖,但是它和 JS 有啥区别呢?

JSX 的本质是 JS 的语法扩展

JSX is a syntax extension to JavaScript. It is similar to a template language, but it has full power of JavaScript.

JSX 既然作为 JS 的一种语法扩展,那肯定就是原始 JS 有些功能无法实现 React 这个工具定制化的功能,或者说为了用户体验,需要在原生 JS 上做一下扩展包装,让用户用起来更爽一点。而根据 React 官方介绍,我觉得为了用户体验更佳,写更少的代码,可能是使用 JSX 的更重要的原因吧。

JSX gets compiled to React.createElement() calls which return plain JavaScript objects called “React elements”.

JSX 会被编译成一个叫 ReactElement 的对象,这个对象就是描述组件的真正主人。So,我们其实可以不用 JSX,而直接使用 React.createElement() 编写组件,这样还省了一层 Babel 转化。

但是我们的组件太复杂,使用对象描述的时候就需要更多属性,层级越深,代码嵌套就越多,可读性就越差。这个时候面对屎山级代码,可能直接就劝退了。 React 为了留住用户,把困难留给自己,把顺畅留给用户,让大家尽可能使用属性的 类HTML 来编写组件,至于复杂的处理,就让 React 自己默默承受吧。

Tip:这也是为什么 React17 之前的所有组件都必须引入 React ,即便你没有显式的使用到 React,因为你编写的 JSX 文件需要用到 React.createElement() 这个方法,而这个方法是在编译阶段, babel 帮你完成的,但是你引入 React,那么就会报错咯。当然 React 17 已经解决了这个问题,只要在根路径使用到了 React 的 entry,后面的组件都会自动处理

这里也就解释了引子中的问题,为什么我们在写 React 组件的时候,习惯性会用 .jsx 作为后缀,因为我们需要给 babel 下个 flag,告诉它我们这边需要编译。当然你不用行不行,当然可以,babel 作为那么牛逼的工具,即便你不告诉它,它也能够根据你写的代码,通过根对象寻址的方式发现你就是一个 React 组件,你编写的是 JSX, 你需要编译成 ReactElement,然后对你进行服务。但是如果你不写,一来可读性差,无法区分普通 js 文件和 JSX 组件,二来确实会对编译效果产生部分影响。

有些问题不是可不可以,是优不优雅。很多代码是可以用且没有太大的问题,但是就是恶心,难受不优雅罢了。同理,有些问题能不能问,回答准不准确,是需要上下文和实际情况的,比方说 React 是同步还是异步的,就是一个典型的为了问而问的问题,真想反问一句,你啥时候用它同步情况了,这样做真的不会被打吗?这个下次再聊。

JSX 如何是如何映射为 DOM 的

React.createElement() 源码

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) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = "" + config.key; //转成字符串
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;

    for (propName in config) {
      if (
        // 筛选出可以放在 props 中的属性
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // 前两个入参分别是 type 和 config,剩下的都是 children
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    // 如果只有一个 child,直接赋值到 props.children
    props.children = children;
  } else if (childrenLength > 1) {
    // 如果是多个 child,则返回一个 child 数组
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (let propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
}
复制代码

分析入参

Babel 将 JSX 转成 React.createElement(type,config,children),

  • type: 用于是不节点类型的参数;可以是 h1div 这些 hostCompoennt 的类型,也可以是 ReactComponent 这种类型
  • config: 组件中所有的属性都会以 config 对象的形式入参,包括 key, ref 这类特殊属性,也包括像 className, xxx 这类自定义属性
  • children: 子元素,子节点,以对象的的形式嵌套入参,里面也包含了 type, config 和 children 属性。

例子:


var element = React.createElement(
  "h1",
  {
    xxx: "111",
    ref: "myDiv",
  },
  React.createElement(
    "p",
    {
      yyy: "222",
    },
    "123"
  )
);
复制代码

对应于 DOM

  <h1 xxx="111" ref="myDiv">
    <p yyy="222">123</p>
  </h1>
复制代码

拆解 createElement 的过程

  1. 入口 React.createElement -- 在 react/ReactElement 文件中
  2. 二次处理特殊的属性 ref, key, self, source
    • 这里 ref 可以注意一下,在我们使用 useRef 做穿透处理的时候,外层入参除了 props,还得单独传入 ref,就是因为 ref 不存储在 props 中
  3. 遍历 config, 筛选出可以提取进 props 中的属性
    • 除了 4个特殊属性和原型链遗传下来的属性,当前属性基本都存储在 props 中
  4. 提取子元素,加入到 props.children 中
    • 可以是单个 children 对象,也可以是对象数组
  5. 格式化 defaultProps
  6. 结合上述参数,通过 ReactElement 再次格式化,然后就成了一个 ReactElement 对象了。

连接虚拟 DOM 和真实 DOM

  • createElement 最后返回的就是虚拟 DOM 树,它是真实 DOM 的一个映射
  • 在 Web 中,我们一般会使用 React.render(element,root) 来将虚拟 DOM 映射到真实 DOM 中去

小结

  • JSX 创建的目的是为了简化创建 ReactElement 的语法糖,原生的 JSX 是不能被浏览器识别的,需要通过 babel 编译成 React.createElement(type,config,children) 的形式, 并最终表示为一个 ReactElement 对象
  • ReactElement 其实就是所谓的虚拟 DOM,根节点其实就是虚拟 DOM 树,因为它是一个树状结构,它是真实 DOM 的映射
  • 在 Web 中,我们一般用 React.render 将 ReactElement 映射到真实 DOM 中去,其中入参需要一个容器 DOM 节点
  • 引子中提到的使用 .jsx 后缀,一来是为了代码规范,方便阅读;二来是为了提示 babel 编译。
  • 在 React 17 之后,即便不引入 React,也可以直接编写 JSX,而不怕 babel 无法是不而报错了。
………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览