JSX 代码是如何“摇身一变”成为 DOM 的?

发布时间 2023-12-05 11:05:41作者: 前端旧约

JSX 是一种语法,并不是 React 中的内容,时下接入 JSX 语法的框架越来越多,但与之缘分最深的仍然是 React。本节来讲一下 React 是如何摇身一变成为 DOM 的。

我们平时在写React时会用 JSX 来描述组件的内容,例如下面的代码中,render 方法 return 的内容就是 JSX 代码。

class App extends React.Component {
  render() {
    return (
      <div className="App">
        <h1 className="title">I am the title</h1>
        <p className="content">I am the content</p>
      </div>
    );
  }
}

我们考虑以下三个问题:

  1. JSX 的本质是什么,它和 JS 之间到底是什么关系?
  2. React 为什么要用 JSX?
  3. JSX 是如何映射为 DOM 的?

这一节我们就将这三个问题一一解答。

1)JSX 的本质是什么?它和JS之间的到底是什么关系?

JSX 到底是什么,我们先来看看 React 官网给出的一段定义:

JSX 是 JavaScript 的一种语法扩展,它和模板语言很接近,但是它充分具备 JavaScript 的能力。

“语法扩展”这一点在理解上几乎不会产生歧义,不过“它充分具备 JavaScript 的能力”这句,却总让人摸不着头脑,JSX 和 JS 怎么看也不像是一路人啊?这就引出了“JSX 语法是如何在 JavaScript 中生效的”这个问题。

JSX 是 JavaScript 的扩展,而不是 JavaScript 的某个版本,因此浏览器并不会天然支持,那么 JSX 是如何在 JavaScript 中生效的呢?

React 官网是这样的解释的:

JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作“React Element”的 JS 对象。

那么 JSX 如何转换成 React.createElement() 的呢?答案就是通过 babel 转换。

我们直接打开 babel playground 来写一段 JSX 代码看一下 babel 转换后的结果。![image-20231204112041472](/Users/jiuyuezhang/Library/Application Support/typora-user-images/image-20231204112041472.png)

可以看到 JSX 代码都被转换成了 React.createElement 调用。

接下来我们总结一下来回答标题提到的两个问题。

JSX 是 JavaScript 的扩展,不是 JavaScipt 的某个版本,需要通过 Babel 进行转换成 JavaScript 代码。

JSX 会被 babel 转换为 React.CreateElement(...) 调用的形式,执行后返回的结果是一个对象。

2)React 为什么要用 JSX?

从上一节我们知道 JSX 等价于一次 React.createElement 调用,那么 React 官方为什么不直接引导我们用 React.createElement 来创建元素呢?

在实际功能效果一致的前提下,JSX 代码层次分明、嵌套关系清晰;而 React.createElement 代码则给人一种非常混乱的“杂糅感”,这样的代码不仅读起来不友好,写起来也费劲。

JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。

3)JSX 是如何映射为 DOM 的?

我们知道 JSX 经过babel转换后会变成 React.createElement(...) 的形式,接下来我们就来一起探讨一下 React.createElement(...) 是如何工作的?

3.1 入参解读:创造一个元素需要知道哪些信息

我们先来看看方法的入参:

export function createElement(type, config, children)

createElement 有 3 个入参,这 3 个入参囊括了 React 创建一个元素所需要知道的全部信息。

  • type:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。
  • config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
  • children:子节点,如果有多个子节点,那么依次往后写。

举个例子:

<ul className="list">
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

经过 Babel 转换后的形式为:

注意:从第三个入参开始往后,传入的参数都是 children

React.createElement("ul", {
  // 传入属性键值对
  className: "list"
}, React.createElement("li", {
  key: "1"
}, "1"), React.createElement("li", {
  key: "2"
}, "2"));

3.2 出参解读:初识虚拟DOM

下面的代码是 React.createElement(...) 调用的返回值格式。

注意:这是 fiber节点之前的每个节点的格式。

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 记录创造该元素的组件
    _owner: owner,
  };

  // 
  if (__DEV__) {
    // 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
  }

  return element;
};

举个例子

const AppJSX = (<div className="App">

  <h1 className="title">I am the title</h1>

  <p className="content">I am the content</p>

</div>)

console.log(AppJSX)

输出为:

这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是老生常谈的“虚拟 DOM”(准确地说,是虚拟 DOM 中的一个节点

既然是“虚拟 DOM”,那就意味着和渲染到页面上的真实 DOM 之间还有一些距离,这个“距离”,就是由大家喜闻乐见的ReactDOM.render方法来填补的。

在每一个 React 项目的入口文件中,都少不了对 React.render 函数的调用。下面我简单介绍下 ReactDOM.render 方法的入参规则:

复制代码

ReactDOM.render(
    // 需要渲染的元素(ReactElement)
    element, 
    // 元素挂载的目标容器(一个真实DOM)
    container,
    // 回调函数,可选参数,可以用来处理渲染结束后的逻辑
    [callback]
)

ReactDOM.render 方法可以接收 3 个参数,其中第二个参数就是一个真实的 DOM 节点这个真实的 DOM 节点充当“容器”的角色,React 元素最终会被渲染到这个“容器”里面去。比如,示例中的 App 组件,它对应的 render 调用是这样的:

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

注意,这个真实 DOM 一定是确实存在的。比如,在 App 组件对应的 index.html 中,已经提前预置 了 id 为 root 的根节点:

<body>
    <div id="root"></div>
</body>