手写 Mini React:深入理解 createElement 和 render 原理

2 阅读4分钟

React 作为现代前端框架的代表,其核心思想包括组件化、响应式、虚拟 DOM 等。想要真正理解 React 的底层原理,最好的方式就是手写一个 Mini React。本文将从 JSX 转译到虚拟 DOM 渲染,带你一步步实现 React 的核心功能。

JSX 的本质

JSX 是 React 最具特色的语法糖,它允许我们在 JavaScript 中直接编写类似 HTML 的标签。这种声明式的写法让代码更加直观:

jsx

let userList = (
  <div>
    <h2>用户列表</h2>
    {users.map(user => <p key={user.id}>{user.name}</p>)}
  </div>
)

但浏览器并不认识 JSX,它需要通过 Babel 转译成 React.createElement 函数调用。这就是我们要实现的第一个核心函数。

实现 createElement 函数

createElement 是整个 React 渲染流程的起点,它接收三个参数:

  • type: 元素类型(如 'div'、'h1' 或组件)
  • props: 元素属性对象
  • children: 子元素(可变参数)

核心实现如下:

javascript

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => 
        typeof child === 'object'
          ? child
          : createTextElement(child)
      )
    }
  }
}

这个函数的关键在于处理 children。我们需要遍历所有子元素,如果子元素已经是对象(虚拟 DOM),则直接使用;如果是字符串或数字,则调用 createTextElement 转换为文本节点。

为什么要统一处理文本节点?

在 React 中,为了统一渲染逻辑,文本节点也需要包装成虚拟 DOM 对象:

javascript

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  }
}

这样做的好处是,无论是元素节点还是文本节点,都遵循相同的数据结构,render 函数可以用统一的方式处理。

虚拟 DOM 的数据结构

经过 createElement 处理后,我们得到的虚拟 DOM 结构是这样的:

javascript

{
  type: 'div',
  props: {
    style: 'background:salmon',
    children: [
      {
        type: 'h1',
        props: {
          children: [
            { type: 'TEXT_ELEMENT', props: { nodeValue: 'Hello, world!', children: [] } }
          ]
        }
      },
      {
        type: 'h2',
        props: {
          style: 'text-align:right',
          children: [
            { type: 'TEXT_ELEMENT', props: { nodeValue: 'from Didact', children: [] } }
          ]
        }
      }
    ]
  }
}

这个树形结构完整描述了 DOM 的层级关系和属性,但它只是 JavaScript 对象,还没有真正渲染到页面上。

实现 render 函数

render 函数负责将虚拟 DOM 转换为真实 DOM 并挂载到页面。它的实现分为几个步骤:

1. 创建 DOM 节点

根据虚拟 DOM 的 type 创建对应的真实节点:

javascript

const dom = element.type === 'TEXT_ELEMENT'
  ? document.createTextNode('')
  : document.createElement(element.type);

文本节点使用 createTextNode,普通元素使用 createElement

2. 添加属性

遍历 props 对象,将属性赋值给 DOM 节点。这里需要过滤掉 children 属性,因为它不是 DOM 的原生属性:

javascript

const isProperty = key => key !== 'children';

Object.keys(element.props)
  .filter(isProperty)
  .forEach(name => {
    dom[name] = element.props[name];
  });

这段代码会将 styleclassName 等属性直接赋值到 DOM 对象上。例如 dom['style'] = 'background:salmon' 等同于 dom.style = 'background:salmon'

3. 递归渲染子元素

对每个 children,递归调用 render 函数:

javascript

element.props.children.forEach(child => render(child, dom));

这里体现了递归的"递"过程——自顶向下创建节点。

4. 挂载到父容器

最后将创建好的 DOM 节点挂载到父容器:

javascript

container.appendChild(dom);

这是递归的"归"过程——自底向上完成挂载。

完整的渲染流程

将所有代码整合,我们就得到了一个完整的 Mini React:

javascript

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => 
        typeof child === 'object'
        ? child
        : createTextElement(child)
      )
    }
  }
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  }
}

function render(element, container) {
  const dom = element.type === 'TEXT_ELEMENT'
    ? document.createTextNode('')
    : 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);
}

window.Didact = {
  createElement,
  render,
}

使用示例

配置 JSX 转译:

javascript

/** @jsxRuntime classic */
/** @jsx Didact.createElement */
const element = (
  <div style="background:salmon">
    <h1>Hello, world!</h1>
    <h2 style="text-align:right">from Didact</h2>
  </div>
)

const container = document.getElementById('root');
Didact.render(element, container);

通过注释 /** @jsx Didact.createElement */,我们告诉 Babel 将 JSX 转译为 Didact.createElement 调用,而不是默认的 React.createElement

React 做了什么?

通过手写这个 Mini React,我们可以更清晰地理解 React 的价值:

  1. 声明式 UI: 开发者只需描述"想要什么样的界面",而不用关心"如何操作 DOM"
  2. 虚拟 DOM: 将 UI 抽象为 JavaScript 对象,为后续的 diff 算法和性能优化打下基础
  3. 统一的数据结构: 通过 createElement 将所有节点统一处理,简化了渲染逻辑

React 帮我们处理了繁琐的 DOM 操作(如重绘、重排),让开发者可以专注于业务逻辑和数据流转。

总结

本文通过实现 createElementrender 两个核心函数,揭示了 React 从 JSX 到真实 DOM 的完整流程:

  1. JSX 经 Babel 转译为 createElement 调用
  2. createElement 生成虚拟 DOM 树
  3. render 递归遍历虚拟 DOM,创建真实节点并挂载

这只是 React 原理的冰山一角。真正的 React 还包括 Fiber 架构、调度器、Hooks、diff 算法等复杂机制。但理解这个 Mini 版本,是深入 React 源码的第一步。

如果你想继续深入,可以尝试实现:

  • 函数组件和类组件的支持
  • 事件系统
  • 简单的 diff 算法
  • 基础的 Hooks

学习框架最好的方式,就是动手实现一个。