《从零手写Mini React》

从零手写Mini React:吃透JSX与虚拟DOM,彻底搞懂React底层渲染逻辑

引言

在前端开发领域,React 早已成为主流的 MVVM 框架,组件化、虚拟 DOM、JSX 语法这些特性我们天天在用,但抛开框架的封装,你是否真的理解:为什么写 JSX 就能渲染出页面?虚拟 DOM 到底是什么?React 渲染的核心流程又是什么?

死记硬背源码概念远不如动手实现来得深刻,今天我们就抛开繁杂的 React 源码,从零实现一个极简版 Mini React(命名为 Didact),只聚焦JSX 编译、虚拟 DOM 生成、真实 DOM 渲染三大核心环节,用几十行代码,撕开 React 底层的神秘面纱,让前端原理学习不再晦涩难懂。


一、先搞懂:React 渲染的核心本质

很多刚接触 React 的同学,都会陷入一个误区:把 JSX 当成 HTML,把虚拟 DOM 当成"优化手段"。其实 React 的核心渲染逻辑,总结起来就一句话:开发者写 JSX 描述 UI → 编译生成虚拟 DOM → 虚拟 DOM 转为真实 DOM 挂载到页面

1.1 关键概念拆解

  • JSX :不是模板语法,更不是 HTML,而是 JavaScript 的语法糖,最终会被 Babel 编译成可执行的 JS 函数调用,让我们能用更直观的方式编写 UI 结构。
  • 虚拟 DOM(VDOM) :一个普通的 JS 对象,用来描述真实 DOM 的结构、属性和子节点,是 React 操作 DOM 的中间层,避免直接操作原生 DOM 导致的性能损耗。
  • 渲染流程 :从 JSX 到页面可见元素,本质是JSX → createElement → VDOM → render → 真实 DOM 的转化过程。

1.2 为什么要手写 Mini React?

  • 剥离 React 源码中冗余的边界处理、兼容逻辑,直击核心原理,学习效率翻倍;
  • 亲手实现每一步逻辑,彻底理解虚拟 DOM 和 JSX 的底层作用,告别"只会用不懂原理"的困境;
  • 夯实前端基础,应对面试中 React 原理类问题得心应手,同时提升源码阅读能力。

二、极简环境搭建:零配置快速上手

我们不需要 create-react-app、Vite 这类复杂脚手架,也不用配置繁琐的 Babel 工程化环境,采用原生 HTML + 在线 Babel的方式,零配置就能运行,全程专注于逻辑实现,不被工程化琐事干扰。

核心准备:新建一个 HTML 文件,引入 Babel 在线编译脚本,用于实时编译 JSX 语法,后续所有核心代码都写在页面内,打开浏览器即可查看效果。


三、Mini React 核心代码实现(逐行精讲)

我们的 Mini React 只实现两大核心函数:createElement(生成虚拟 DOM)和 render(渲染真实 DOM),再搭配文本节点处理函数,完成完整渲染流程。

3.1 第一步:处理文本节点,统一 DOM 格式

真实 DOM 分为普通元素节点(div、h1 等)和文本节点,二者创建方式不同,为了让后续渲染逻辑统一处理,我们需要把纯文本也封装成格式一致的虚拟 DOM 对象,单独封装处理函数。

php 复制代码
/**
 * 创建文本类型的虚拟DOM
 * @param {string} text - 文本内容
 * @return {object} 标准化文本虚拟DOM
 */
function createTextElement(text) {
  return {
    // 标记为文本节点,区分普通元素
    type: 'TEXT_ELEMENT',
    props: {
      // 文本节点的内容,对应原生DOM的nodeValue
      nodeValue: text,
      // 文本节点无子节点,固定为空数组
      children: []
    }
  }
}

这一步的核心作用:抹平元素节点和文本节点的差异,让后续递归渲染无需区分处理,简化代码逻辑。

3.2 第二步:实现 createElement,生成虚拟 DOM

Babel 编译 JSX 后,会自动调用 React.createElement 方法,我们手写 Mini React,只需实现同名逻辑的 Didact.createElement,就能承接编译后的 JSX。

该函数接收三个参数:节点类型(type)、节点属性(props)、子节点(children) ,最终返回标准化的虚拟 DOM 对象。

typescript 复制代码
/**
 * 创建虚拟DOM
 * @param {string|function} type - 节点类型(标签名/组件)
 * @param {object} props - 节点属性(style、class等)
 * @param  {...any} children - 子节点
 * @return {object} 虚拟DOM对象
 */
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      // 展开传入的属性
      ...props,
      // 处理子节点:递归格式化,保证所有子节点都是虚拟DOM对象
      children: children.map(child => {
        // 如果是对象,说明已是虚拟DOM,直接返回
        // 如果是基础类型(文本),调用函数封装为虚拟DOM
        return typeof child === 'object' ? child : createTextElement(child)
      })
    }
  }
}

生成的虚拟 DOM 结构

经过该函数处理后,无论元素还是文本,都会生成结构一致的 JS 对象,这就是虚拟 DOM,后续渲染全靠它:

go 复制代码
{
  type: 'div' | 'h1' | 'TEXT_ELEMENT', // 节点类型
  props: {
    style: 'xxx', // 节点属性
    children: [] // 子虚拟DOM数组
  }
}

3.3 第三步:实现 render 函数,虚拟 DOM 转真实 DOM

render 是 Mini React 的渲染入口函数 ,负责把虚拟 DOM 转化为浏览器可识别的真实 DOM,并挂载到指定容器中,核心逻辑是递归遍历虚拟 DOM 树,逐个创建真实节点、绑定属性、拼接子节点。

javascript 复制代码
/**
 * 虚拟DOM渲染为真实DOM并挂载
 * @param {object} element - 虚拟DOM对象
 * @param {HTMLElement} container - 真实DOM挂载容器
 */
function render(element, container) {
  // 1. 根据虚拟DOM类型,创建对应的真实DOM节点
  const dom = element.type === 'TEXT_ELEMENT'
    ? document.createTextNode('') // 创建文本节点
    : document.createElement(element.type) // 创建普通元素节点

  // 2. 为真实DOM绑定属性,过滤掉children(非原生DOM属性)
  const isNormalProp = key => key !== 'children'
  Object.keys(element.props)
    .filter(isNormalProp)
    .forEach(propName => {
      dom[propName] = element.props[propName]
    })

  // 3. 递归渲染所有子节点,挂载到当前DOM
  element.props.children.forEach(child => render(child, dom))

  // 4. 将构建好的真实DOM,挂载到页面指定容器
  container.appendChild(dom)
}

3.4 第四步:全局挂载 + Babel 配置

想要让 Babel 识别我们自定义的 Mini React,需要将核心函数挂载到 window 全局,同时通过注释配置 Babel 编译规则,告诉 Babel:JSX 要编译成 Didact.createElement 而非默认的 React.createElement

php 复制代码
// 挂载到全局,命名为Didact,统一Mini React命名空间
window.Didact = {
  createElement,
  render
}

// Babel编译配置:经典JSX运行时 + 指定createElement函数
/** @jsxRuntime classic */
/** @jsx Didact.createElement */

四、完整可运行代码(复制即用)

将以下代码复制到 HTML 文件中,直接用浏览器打开,即可看到渲染效果,无需任何额外配置,新手也能轻松上手实操。

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>手写Mini React - Didact</title>
  <!-- 引入在线Babel,实时编译JSX -->
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <!-- 渲染挂载节点 -->
  <div id="root"></div>

  <!-- type="text/babel" 标识该脚本需要Babel编译 -->
  <script type="text/babel">
    // 1. 文本虚拟DOM创建
    function createTextElement(text) {
      return {
        type: 'TEXT_ELEMENT',
        props: {
          nodeValue: text,
          children: []
        }
      }
    }

    // 2. 核心:创建虚拟DOM
    function createElement(type, props, ...children) {
      return {
        type,
        props: {
          ...props,
          children: children.map(child => 
            typeof child === 'object' ? child : createTextElement(child)
          )
        }
      }
    }

    // 3. 核心:渲染真实DOM
    function render(element, container) {
      const dom = element.type === 'TEXT_ELEMENT'
        ? document.createTextNode('')
        : document.createElement(element.type)

      // 绑定属性
      Object.keys(element.props)
        .filter(key => key !== 'children')
        .forEach(prop => dom[prop] = element.props[prop])

      // 递归渲染子节点
      element.props.children.forEach(child => render(child, dom))
      container.appendChild(dom)
    }

    // 全局挂载Didact
    window.Didact = { createElement, render }

    // Babel配置
    /** @jsxRuntime classic */
    /** @jsx Didact.createElement */

    // 测试UI:编写JSX
    const demoElement = (
      <div style="width: 400px; margin: 50px auto; padding: 24px; background: #ff9e7b; border-radius: 12px;">
        <h1 style="color: #fff; margin: 0 0 12px;">Hello Mini React👋</h1>
        <p style="color: #fff; font-size: 16px; text-align: right; margin: 0;">我是手写的 Didact</p>
      </div>
    )

    // 获取挂载容器,执行渲染
    const root = document.getElementById('root')
    Didact.render(demoElement, root)
  </script>
</body>
</html>

实操小贴士:直接打开 HTML 文件可能出现跨域问题,推荐使用 VSCode 的 Live Server 插件启动本地服务,一键运行无报错。


五、核心原理复盘:吃透就等于懂了 React 根基

  1. JSX 不是魔法 :我们写的 <div>xxx</div>,最终会被 Babel 编译成 Didact.createElement('div', {}, ...children),所谓的 JSX 语法,只是让 UI 编写更便捷的语法糖。
  2. 虚拟 DOM 不神秘 :就是一个普通 JS 对象,作用是用 JS 描述 DOM,统一节点格式,避免频繁操作原生 DOM 引发的重绘重排,同时方便后续做 DOM 差异化更新。
  3. 渲染核心是递归:虚拟 DOM 是树形结构,想要渲染完整 UI,必须通过递归遍历,逐层创建真实节点,最终拼接成完整 DOM 树挂载到页面。
  4. React 底层极简 :抛开进阶特性,React 最核心的就是虚拟 DOM 生成 + 递归渲染,我们手写的 Mini React,已经涵盖了这一核心逻辑。

六、进阶方向:如何完善 Mini React?

本次实现的是极简版 Mini React,仅完成基础渲染,想要更贴近真实 React,还可以在此基础上扩展:

  • 实现 diff 算法:对比新旧虚拟 DOM,只更新变化的节点,而非全量重新渲染,提升性能;
  • 实现 类组件/函数组件:支持自定义组件,贴合实际开发场景;
  • 实现 setState 状态更新:完成数据驱动视图的核心响应式逻辑;
  • 实现 Fiber 调度机制:解决递归渲染的性能瓶颈,实现可中断渲染。

结语

学习前端框架原理,动手实践永远是最好的方式。这几十行代码,没有复杂的封装和晦涩的逻辑,却能让你彻底吃透 React 最核心的渲染流程,看懂虚拟 DOM 和 JSX 的本质。

相关推荐
www_stdio2 小时前
手搓一个 Mini React:从 JSX 到虚拟 DOM 的完整实现
前端·react.js·面试
@大迁世界2 小时前
01.什么是 ReactJS?
前端·javascript·react.js·前端框架·ecmascript
默 语3 小时前
TypeScrip+React 全栈生态实战:从架构选型到工程落地,告别开发踩坑
前端·react.js·架构
zhengzhengwang3 小时前
react18升级新特性
前端·javascript·react.js
炒毛豆4 小时前
微前端框架 qiankun 简明指南
前端·javascript·react.js
我命由我123456 小时前
React - 验证 Diffing 算法、key 的作用
javascript·算法·react.js·前端框架·html·html5·js
湛海不过深蓝13 小时前
【procomponents】根据表单查询表格数据的两种写法
前端·javascript·react.js
Beth_Chan13 小时前
Stock Trading - React
javascript·react.js
大雷神13 小时前
HarmonyOS APP<玩转React>开源教程二:ArkTS 语言基础
react.js·开源·harmonyos