【React源码01】深入学习React 源码实现——JSX与虚拟DOM生成

深入学习 React 源码实现之 JSX与虚拟DOM生成


📘 一、JSX 简介与历史演进

1.1 什么是 JSX?

JSX(JavaScript XML) 是一种 JavaScript 的语法扩展,允许开发者在 JavaScript 中直接书写类似 HTML 或 XML 的结构。

const element = <h1>Hello, world!</h1>;

1.2 JSX 的起源

  • 诞生背景:Facebook 在开发 React 时发现传统的模板语言无法满足组件化开发需求。
  • 首次亮相:React v0.12 版本中正式引入 JSX,并通过 Babel 插件 @babel/plugin-transform-react-jsx 将其转换为标准的 React.createElement() 调用。
  • 标准化尝试:目前 JSX 本身并不是 ECMAScript 标准的一部分,但已广泛被社区接受。

1.3 JSX 的本质

JSX 最终会被编译成 React.createElement(type, props, children) 函数调用:

// JSX
const element = <h1 className="greeting">Hello, world!</h1>;

// 编译后
const element = React.createElement(
  'h1',
  { className: 'greeting' },
  'Hello, world!'
);

📂 二、React 源码中的 JSX 实现路径

2.1 JSX 转换核心源码文件

文件路径功能
packages/react/src/ReactElement.js定义 React.createElement() 方法
packages/babel-plugin-transform-react-jsx/Babel 插件将 JSX 转换为 React.createElement() 调用

2.2 核心函数解析:createElement()

这是一个简化版的 React createElement 函数实现,用于创建 React 元素。下面是每行代码与详细注释:

// packages/react/src/ReactElement.js

// 导出一个名为 createElement 的函数
export function createElement(type, config, children) {
  // 声明一个变量用于存储属性名
  let propName;

  // 初始化一个空对象用于存储属性
  const props = {};

  // 声明两个变量用于处理 key 和 ref
  let key = null;
  let ref = null;

  // 检查 config 是否存在(非 null 或 undefined)
  if (config != null) {
    // 检查 config 是否有有效的 ref 属性
    if (hasValidRef(config)) {
      // 如果有,将 ref 赋值给 ref 变量
      ref = config.ref;
    }
    // 检查 config 是否有有效的 key 属性
    if (hasValidKey(config)) {
      // 如果有,将 key 转换为字符串并赋值给 key 变量
      key = '' + config.key;
    }

    // 遍历 config 对象的所有属性
    for (propName in config) {
      // 检查属性是否是 config 对象自身的属性(非继承的)
      // 并且不是保留属性(RESERVED_PROPS 中的属性)
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        // 将非保留属性复制到 props 对象中
        props[propName] = config[propName];
      }
    }
  }

  // 计算子元素的数量(arguments.length - 2 因为 type 和 config 是前两个参数)
  const childrenLength = arguments.length - 2;
  
  // 处理只有一个子元素的情况
  if (childrenLength === 1) {
    // 直接将子元素赋值给 props.children
    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
    props.children = childArray;
  }

  // 返回一个 ReactElement 对象
  return ReactElement(
    type,       // 元素类型(如 'div'、'span' 或自定义组件)
    key,        // 元素的 key(用于列表渲染时的标识)
    ref,        // 元素的 ref(用于访问 DOM 节点或组件实例)
    undefined,  // 通常用于内部使用(self)
    undefined,  // 通常用于内部使用(source)
    ReactCurrentOwner.current, // 当前正在创建元素的组件
    props       // 元素的属性对象
  );
}

老曹笔记🚀

  1. hasValidRef 和 hasValidKey 是假设存在的辅助函数,用于验证 ref 和 key 的有效性
  2. RESERVED_PROPS 是一个包含 React 保留属性名的对象(如 key、ref 等)
  3. ReactElement 是一个内部函数,用于创建最终的 React 元素对象
  4. ReactCurrentOwner.current 用于跟踪当前正在创建元素的组件(在 React 的上下文中)
  5. 这个实现是 React 16 之前版本的简化版本,现代 React 的实现更加复杂,包含了更多的优化和特性

2.3 ReactElement 构造函数

这是一个简化版的 ReactElement 工厂函数实现,用于创建 React 元素对象。下面是每行代码的详细注释:

// 定义一个名为 ReactElement 的函数,接收多个参数
const ReactElement = function(type, key, ref, self, source, owner, props) {
  // 创建一个新的对象,表示 React 元素
  const element = {
    // 使用特殊符号 $$typeof 标记这是一个 React 元素
    // REACT_ELEMENT_TYPE 是一个内部常量,用于唯一标识 React 元素类型
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,// 元素的类型,可以是原生 DOM 标签名(如 'div')或自定义组件
    key: key, // 元素的 key,用于在列表渲染时帮助 React 识别哪些元素发生了变化    
    ref: ref,// 元素的 ref,用于访问 DOM 节点或组件实例
    props: props,  // 元素的属性对象,包含传递给组件的所有 props  
    _owner: owner, // 指向创建该元素的组件(通常用于开发工具和错误信息)
  };
  
  // 返回创建的 React 元素对象
  return element;
};

补充说明:

  1. REACT_ELEMENT_TYPE 是一个内部常量,用于唯一标识 React 元素类型,通常是一个 Symbol(如 Symbol.for('react.element')
  2. selfsource 参数在这个简化版本中没有被使用,但在完整的 React 实现中,它们可能用于开发目的(如调试信息)
  3. _owner 属性通常指向创建该元素的组件,这在 React 开发工具中用于显示组件层次结构
  4. 这个函数创建的对象是一个普通的 JavaScript 对象,不是真正的 DOM 元素
  5. React 使用这种结构来描述 UI 的虚拟表示(虚拟 DOM),然后由 React 的 reconciliation 算法决定如何更新实际 DOM

🔍 三、设计模式分析

3.1 工厂模式(Factory Pattern)

  • React.createElement() 是一个典型的工厂方法,封装了创建 React 元素的逻辑。
  • 用户无需关心底层构造细节,只需传入类型和属性即可。

3.2 抽象语法树(AST)构建思想

  • JSX 经过 Babel 解析后生成 AST;
  • 再由 Babel 插件将其转换为 React.createElement() 调用;
  • 这种“编译器+运行时”的方式体现了前端工程化的抽象思维。

3.3 单例模式(Singleton)

  • ReactElement 创建的对象是不可变的(Immutable),保证了渲染过程的一致性。

💡 四、完整 JSX 实现代码示例(Mini-React)

我们来手动实现一个简化版的 JSX 到 Virtual DOM 的转换器。

4.1 自定义 createElement

// 定义一个名为 createElement 的函数,接收三个参数:
// type - 元素类型(如 'div' 或自定义组件)
// props - 元素的属性对象
// ...children - 剩余参数,表示子元素
function createElement(type, props, ...children) {
  // 返回一个对象,表示 React 元素
  return {
    // 元素类型
    type,
    // 元素属性对象
    props: {
      // 展开传入的 props 对象
      ...props,
      // 处理子元素
      children: children.map(child =>
        // 检查子元素是否是对象(即是否是 React 元素)
        typeof child === "object" 
          ? child 
          // 如果不是对象,则创建文本元素
          : createTextElement(child)
      ),
    },
  };
}

// 定义一个名为 createTextElement 的函数,用于创建文本元素
function createTextElement(text) {
  // 返回一个对象,表示文本元素
  return {
    // 固定类型为 "TEXT_ELEMENT",用于区分普通元素
    type: "TEXT_ELEMENT",
    // 文本元素的属性对象
    props: {
      // 存储文本内容
      nodeValue: text,
      // 文本元素没有子元素,所以设为空数组
      children: [],
    },
  };
}

4.2 渲染函数

算法思路

  1. DOM 节点创建: 根据 element.type 创建相应的 DOM 节点。 如果是
    “TEXT_ELEMENT”,则创建一个文本节点。 否则,创建一个普通 DOM 元素(如 div、span 等)。
  2. 属性处理: 使用 Object.keys(element.props) 获取元素的所有属性名。 通过
    filter(isProperty) 过滤掉 “children” 属性,因为 “children” 是子元素,不是 DOM 属性。
    遍历剩余的属性名,并将属性值赋给 DOM 节点。
  3. 子元素渲染: 遍历 element.props.children,递归调用 render 函数渲染每个子元素。 子元素会被附加到当前
    DOM 节点中。
  4. 附加到容器: 最后,将渲染好的 DOM 节点附加到传入的 container 节点中。
// 定义一个名为 render 的函数,接收两个参数:
// element - 要渲染的 React 元素
// container - 容器 DOM 节点,渲染结果将被附加到这个节点中
function render(element, container) {
  // 创建一个 DOM 节点
  const dom =
    // 检查元素类型是否是 "TEXT_ELEMENT"(文本元素)
    element.type === "TEXT_ELEMENT"
      ? // 如果是,创建一个文本节点
        document.createTextNode("")
      : // 否则,创建一个普通 DOM 元素
        document.createElement(element.type);

  // 定义一个辅助函数,用于判断属性名是否是有效的 DOM 属性(排除 "children")
  const isProperty = key => key !== "children";

  // 获取元素的所有属性名,过滤掉 "children" 属性
  Object.keys(element.props)
    .filter(isProperty)
    // 遍历剩余的属性名
    .forEach(name => {
      // 将属性值赋给 DOM 节点
      dom[name] = element.props[name];
    });

  // 遍历所有子元素
  element.props.children.forEach(child => {
    // 递归渲染每个子元素,并将其附加到当前 DOM 节点
    render(child, dom);
  });

  // 将渲染好的 DOM 节点附加到容器节点中
  container.appendChild(dom);
}

4.3 使用示例

/** @jsx createElement */
const element = (
  <div id="foo">
    <a href="#">Click me</a>
    <b>Hello</b>
  </div>
);

render(element, document.getElementById("root"));

❓ 五、10大 JSX 高频面试题

下面是对 10 大 JSX 高频面试题 的详细讲解,涵盖每个问题的核心知识点和实际应用技巧,适合准备react前端面试的同学深入理解。


1. JSX 是什么?它是如何工作的?

JSX(JavaScript XML) 是一种 JavaScript 的语法扩展,允许开发者在 JS 文件中像写 HTML 一样编写结构代码。

const element = <h1>Hello, world!</h1>;

实际上,JSX 并不是标准的 JavaScript 语法,它需要通过 Babel 插件(如 @babel/plugin-transform-react-jsx 转换为合法的 JS 表达式:

const element = React.createElement('h1', null, 'Hello, world!');

✅ 核心点:JSX 是语法糖,最终会被编译成 React.createElement() 函数调用。


2. React.createElement() 有什么参数?

React.createElement() 是 React 构建 Virtual DOM 的核心函数。它的基本签名如下:

React.createElement(type, [props], [...children])
  • type: 元素类型,可以是字符串(如 'div')、组件类(如 MyComponent),也可以是 Fragment。
  • props: 属性对象,包含元素的所有属性(如 className, style, 自定义属性等)。
  • children: 子元素,可以是一个或多个节点,也可以是文本、数字等。

示例:

<div className="box" style={{ color: 'red' }}>Hello</div>

转换为:

React.createElement('div', { className: 'box', style: { color: 'red' } }, 'Hello');

✅ 核心点:掌握 createElement 的参数结构有助于理解 React 的渲染机制。


3. 为什么不能使用 if 语句在 JSX 中?

JSX 是一个表达式(expression),而不是语句(statement)。因此你不能直接在 JSX 中使用如 iffor 这样的控制流语句。

错误示例:

{
  if (condition) {
    <p>显示内容</p>
  }
}

但你可以使用三元运算符或立即执行函数来实现逻辑判断:

✅ 正确做法:

{ condition ? <p>显示内容</p> : null }

// 或者使用 IIFE
{
  (() => {
    if (condition) return <p>显示内容</p>;
    return null;
  })()
}

✅ 核心点:JSX 中只能嵌入表达式,不能直接写语句。


4. 如何在 JSX 中使用变量?

在 JSX 中,你可以通过 {} 插入任何合法的 JavaScript 表达式,包括变量、函数调用、数组等。

示例:

const name = 'Alice';
<p>Hello, {name}</p>

// 使用数组 map 渲染列表
const items = ['Apple', 'Banana', 'Orange'];
<ul>{items.map(item => <li key={item}>{item}</li>)}</ul>

✅ 核心点:JSX 中的 {} 可以插入任意合法 JS 表达式,但不能直接写语句。


5. JSX 中如何绑定事件?

在 JSX 中绑定事件时,必须使用 驼峰命名法,并且传入的是一个函数引用,而不是字符串。

正确示例:

<button onClick={handleClick}>点击我</button>

注意不要加括号,否则会立即执行函数:

❌ 错误写法:

<button onClick={handleClick()}>点击我</button> // 立即执行了 handleClick

如果你需要传参,可以用箭头函数包裹:

<button onClick={() => handleClick(id)}>点击我</button>

✅ 核心点:事件名使用驼峰命名,传入函数引用而非调用。


6. JSX 如何渲染列表?

通常我们使用 .map() 方法将数组映射为一组 JSX 元素,并返回给 JSX 渲染。

示例:

const list = ['Item 1', 'Item 2', 'Item 3'];

<ul>
  {list.map((item, index) => (
    <li key={index}>{item}</li>
  ))}
</ul>

⚠️ 注意:

  • 每个列表项必须有唯一的 key 属性;
  • 不要使用索引作为 key,除非数据是静态且不会变化。

7. key 的作用是什么?

key 是 React 用来识别哪些元素发生了变化、被添加或删除的关键标识符。

当你在 .map() 中渲染列表时,React 会根据 key 来决定是否复用已有 DOM 节点,从而提升性能。

示例:

{items.map(item => <TodoItem key={item.id} item={item} />)}

✅ 核心点:

  • key 帮助 React 高效地更新虚拟 DOM;
  • 推荐使用唯一 ID 作为 key,而不是索引;
  • 如果没有唯一 ID,可以考虑使用 uuid 或其他生成方式。

8. 如何在 JSX 中写注释?

JSX 中不能使用 HTML 注释 <!-- -->,也不能使用单行注释 //,因为它们都不是合法的 JSX 表达式。

正确写法是使用 {/* ... */} 包裹注释内容:

{/* 这是一个注释 */}
<div>正常内容</div>

多行注释也适用:

{/*
  这是
  多行
  注释
*/}

✅ 核心点:在 JSX 中使用 {/* ... */} 编写注释。


9. Fragment 有什么作用?

Fragment 是 React 提供的一个特殊组件,用于包裹多个 JSX 元素而不引入额外的 DOM 节点。

通常我们这样写:

return (
  <div>
    <h1>Title</h1>
    <p>Paragraph</p>
  </div>
);

但如果我们不想添加额外的 <div>,可以使用 Fragment

import React from 'react';

return (
  <React.Fragment>
    <h1>Title</h1>
    <p>Paragraph</p>
  </React.Fragment>
);

还可以简写为:

<>
  <h1>Title</h1>
  <p>Paragraph</p>
</>

✅ 核心点:Fragment 可以避免额外的 DOM 节点,常用于组件返回多个根元素。


10. 如何避免 XSS 攻击?

React 在默认情况下会对所有插入到 JSX 中的内容进行自动转义,防止跨站脚本攻击(XSS)。

例如:

const userInput = '<script>alert("XSS")</script>';
<div>{userInput}</div>

这段代码不会执行恶意脚本,而是原样输出为文本。

但如果使用 dangerouslySetInnerHTML 强制插入 HTML,则需要特别小心:

<div dangerouslySetInnerHTML={{ __html: userInput }} />

✅ 核心点:

  • React 默认对内容进行转义,防止 XSS;
  • 尽量避免使用 dangerouslySetInnerHTML
  • 如果必须使用,确保内容是可信来源并经过清理。

🎁 六、总结与推荐资源

✅ 下一步学习路径

  1. 先掌握 JSX 的基本语法;
  2. 理解 JSX → React.createElement() 的转换过程;
  3. 阅读 React 源码中 ReactElement.js 的实现;
  4. 尝试手写一个简易 JSX 到 Virtual DOM 的转换器;
  5. 学习 Babel 插件开发,深入理解 AST;
  6. 最终目标:阅读 React Fiber 架构源码,理解整个渲染流程。

📚 推荐参考资料

类型名称地址
文档React 官方文档https://react.dev
GitHubBuild Your Own Reacthttps://github.com/pomber/build-your-own-react
视频Lin Clark - React Conf 2017YouTube - React Conf 2017
社区React Chinahttps://react-china.org/
工具Babel Playgroundhttps://babeljs.io/repl
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈前端老曹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值