【React进阶系列第二课】JSX 与 React Element

JSX 与 React Element

前面说,组件即函数,现在,我们来看看组件函数的返回值:JSX,即 React Element。

JSX 是 createElement 的语法糖

JSX 是 React.createElement 的语法糖。

jsx 复制代码
export default function Post(props) {
  const { title, content } = props;
  return (
    <div className="post">
      <h1 className="post-title">{title}</h1>
      <p className="post-content">{content}</p>
    </div>
  );
}

等价于:

jsx 复制代码
import React from "react";

export default function Post(props) {
  const { title, content } = props;
  return React.createElement(
    "div",
    { className: "post" },
    React.createElement("h1", { className: "post-title" }, title),
    React.createElement("p", { className: "post-content" }, content)
  );
}

React 在文档中也提到,可以不使用 JSX 语法,直接使用 React.createElement。不过,没有人手敲代码时这么做,只有工具生成的代码才会直接输出成 React.createElement,省去二次编译转化过程。

JSX 的编译转化

JSX 是 JavaScript 的拓展语法,无法在浏览器中运行,需要通过 Babel 等工具进行编译转化。

Babel 在线工具 能看到编译后的结果。

对上面的例子,得到的结果是:

js 复制代码
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export default function Post(props) {
  const { title, content } = props;
  return /*#__PURE__*/ _jsxs("div", {
    className: "post",
    children: [
      /*#__PURE__*/ _jsx("h1", {
        className: "post-title",
        children: title,
      }),
      /*#__PURE__*/ _jsx("p", {
        className: "post-content",
        children: content,
      }),
    ],
  });
}

可以看到,这里做了两步转化:

  1. 自动从 react 中引入 jsx/jsxs 函数。
  2. 把 JSX 语法转化为 jsx/jsxs 函数调用。

jsx/jsxs 函数的功能和 React.createElement 类似,都是创建并返回 React 元素(Element)对象。

顺便一提,在 React 17 之前,JSX 是直接编译成 React.createElement 的。

js 复制代码
export default function Post(props) {
  const { title, content } = props;
  return React.createElement(
    "div",
    { className: "post" },
    React.createElement("h1", { className: "post-title" }, title),
    React.createElement("p", { className: "post-content" }, content)
  );
}

注意,此时并没有自动引入 React,需要我们自己手动引入,否则运行时会报错找不到 React,用过 React 16 的同学应该都遇到过这个问题。

React 元素对象

无论是jsx/jsxs(...),还是 React.createElement(...),结果都是 React 元素对象。 它的主要结构如下:

js 复制代码
{
  // 元素的类型。
  // html节点时,是字符串,比如 "div";
  // 自定义组件时,是函数或类的引用
  type: type,
  props: object, // 组件props,包括 children 属性
  //...(其他我们不关注的属性)
}

比如,

jsx 复制代码
<Post title={"Hello World"} content={"This is a post."} />
// 对应的元素对象大概长这样:
{
  type: Post,
  props: {
    title: 'Hello World',
    content: 'This is a post.',
  }
}

<h1>Hello World</h1>
// 对应的元素对象大概长这样:
{
  $$typeof: Symbol(react.element),
  type: 'h1',
  props: {
    children: ['Hello World'],
  }
}

这个就是我们经常听到的虚拟 DOM 节点了。React 元素对象有两个特点:

  • 轻量。相对浏览器 DOM,它的体积很小,创建和其他操作的成本更小。
  • 不可变。元素一旦创建,就不能再修改,随意修改元素会导致虚拟 DOM 比对算法失效

现在,我们知道了: 一段 JSX 就是一个 React 元素对象

以后看 JSX 语法时,就可以把它看成是一个对象,语法上,除了无法修改,和普通的 JavaScript 对象一样,可以赋值、传参、返回等。

元素无法修改,不过,React.cloneElement(element, props, ...children) 可以复制一个相同类型,但不同 props 的元素,有时候会用到。

React 元素与渲染过程

前面说,JSX 创建了元素对象,但是,元素只是通过 type 属性保存了组件函数,组件函数并没有在这个过程中被执行 。也就是说,<Post /> 没有执行 Post() 函数。

你可能会奇怪,如果这个过程没有执行,那会在什么时候执行?答案是,在渲染时。具体点,如果组件函数返回了元素,那 React 在渲染时,就会执行元素对应的组件函数

举个例子:

jsx 复制代码
import { createRoot } from "react-dom/client";

function App() {
  return <Post title="Hello World" content="This is a post." />;
}

createRoot(document.getElementById("root")).render(<App />);

我们已经知道,<App /> 等效于 {type:App,...},所以,整个 render 过程就是:

  1. 执行 render({type:App,...})
  2. 调用 App(),得到 <Post />,即 {type:Post,...}
  3. 执行 render({type:Post,...})
  4. 调用 Post({title:'Hello World',content:'This is a post.'}),得到 <div className="post"><h1 className="post-title">Hello World</h1><p className="post-content">This is a post.</p></div>
  5. 执行 render({type:'div',props:{children:[{type:'h1',...},{type:'p',...]}}),创建对应的浏览器 DOM 节点
  6. 挂载浏览器 DOM 节点,呈现出内容。

上面是简化版的,为了方便理解,实际的渲染过程会复杂很多。

实现 createElement 和 render

有了这些理解,最后,手动实现一个 createElementrender 函数,加深一下。

jsx 复制代码
// file: mreact.js

function createElement(type, props, ...children) {
  //简单地创建并返回一个元素对象
  return {
    type,
    props: {
      ...props,
      children,
    },
  };
}

function render(element, container) {
  // null,undefined,false 不渲染
  if (element === null || element === undefined || element === false) {
    return;
  }

  const isElement = typeof element === "object" && "type" in element; // 这种判断实际中不严谨
  // 非 element 直接,直接以字符串渲染
  if (!isElement) {
    const textNode = document.createTextNode(String(element));
    container.appendChild(textNode);
    return;
  }

  //函数组件,则调用函数,递归渲染
  if (typeof element.type === "function") {
    const nextElement = element.type(element.props);
    render(nextElement, container);
    return;
  } else if (typeof element.type === "string") {
    // 基本元素类型,渲染对应的html元素
    const dom = document.createElement(element.type);
    const isProperty = (key) => key !== "children";
    Object.keys(element.props)
      .filter(isProperty)
      .forEach((name) => {
        dom[name] = element.props[name];
      });
    element.props.children.forEach((child) => render(child, dom));
    container.appendChild(dom);
    return;
  }
}

export default { render, createElement };

这样使用:

js 复制代码
// file: index.js

import mReact from "./mreact.js";

function Post(props) {
  const { title, content } = props;
  return mReact.createElement(
    "div",
    { className: "post" },
    mReact.createElement("h1", { className: "post-title" }, title),
    mReact.createElement("p", { className: "post-content" }, content)
  );
}

mReact.render(
  mReact.createElement(Post, {
    title: "Hello World",
    content: "This is a post.",
  }),
  document.getElementById("root")
);

总结

  • JSX 是 React.createElement 的语法糖。
  • JSX 语法需要经过 Babel 等工具编译转化。
  • 一段 JSX 等效于一个 React Element 对象,也就是虚拟 DOM 对象。
  • 创建 React Element 对象时,不会执行组件函数。
  • 组件函数返回 React Element 对象,React 在渲染时,递归执行组件函数,创建 React Element,构造出虚拟 DOM 树。

拓展案例

React 练习:实现一个 SelectPanel 组件

相关推荐
Book_熬夜!4 分钟前
CSS—补充:CSS计数器、单位、@media媒体查询
前端·css·html·媒体
几度泥的菜花1 小时前
如何禁用移动端页面的多点触控和手势缩放
前端·javascript
狼性书生1 小时前
electron + vue3 + vite 渲染进程到主进程的双向通信
前端·javascript·electron
肥肠可耐的西西公主1 小时前
前端(AJAX)学习笔记(CLASS 4):进阶
前端·笔记·学习
拉不动的猪1 小时前
Node.js(Express)
前端·javascript·面试
Re.不晚1 小时前
Web前端开发——HTML基础下
前端·javascript·html
几何心凉2 小时前
如何处理前端表单验证,确保用户输入合法?
前端·css·前端框架
浪遏2 小时前
面试官😏: 讲一下事件循环 ,顺便做道题🤪
前端·面试
Joeysoda2 小时前
JavaEE进阶(2) Spring Web MVC: Session 和 Cookie
java·前端·网络·spring·java-ee
小周同学:3 小时前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。
前端·npm·node.js