从零开始实现一个简易版 React:构建你自己的前端框架

102 阅读3分钟

从头开始构建一个只支持函数式组件和最基本的 useState 功能的简易版 React,不包含diff算法和fiber架构,这两部分后续单独输出。

1. 设计目标

我们将构建一个极简的前端框架,具备以下基本功能:

  • 创建虚拟 DOM(VDOM)。
  • 渲染虚拟 DOM 到真实 DOM。
  • 支持函数式组件。
  • 简单的状态管理(useState)。

2. 创建虚拟 DOM

虚拟 DOM 是 React 的核心之一,它是一个 JavaScript 对象,描述了 UI 结构。React 会先在内存中更新虚拟 DOM,再与实际 DOM 比对,最终高效地更新浏览器的页面。

在我们的简易版中,虚拟 DOM 只是一个普通的 JavaScript 对象。每个元素对象包含:

  • type: 元素的类型,如 div, h1, button 等。
  • props: 元素的属性,如 className, onclick, style,children 等。

这里举一个虚拟dom节点的示例:

{
  type: 'div',  
  props: {
    className: 'container',  
    style: { color: 'blue' },  
    children: [               
      {
        type: 'h1',            
        props: {
          children: ['你好']  
        }
      },
      {
        type: 'button',        
        props: {
          onclick: handleClick,  
          children: ['按钮']
        }
      }
    ]
  }
}

  • 声明createElement 函数用于创建虚拟 DOM。它的第一个参数是元素类型(如 div),第二个是属性对象,第三个及以后的参数是子元素(可以是其他元素或文本内容)。
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.flat(),
    },
  };
}

3. 渲染虚拟 DOM 到实际 DOM

将虚拟 DOM 渲染成实际的 DOM 是 React 的核心功能之一。我们通过 render 函数遍历虚拟 DOM,生成实际 DOM 元素,并将它们插入到页面中。

  • render 函数接受虚拟 DOM 和容器元素,生成真实的 DOM 元素并插入到容器中。

function render(vdom, container) {
  // 如果 vdom 是字符串(文本节点),直接创建并插入文本节点
  if (typeof vdom === "string") {
    container.appendChild(document.createTextNode(vdom));
    return;
  }

  // 创建实际的 DOM 元素
  const domNode = document.createElement(vdom.type);

  // 遍历虚拟 DOM 的属性(props),并设置到真实 DOM 元素上
  for (const [key, value] of Object.entries(vdom.props)) {
    if (key === "children") {
      // 如果属性是 "children",递归渲染子节点并插入到当前元素
      value.forEach(child => render(child, domNode));
    } else {
      // 否则直接设置 DOM 元素的属性,如 className、id、onclick 等
      domNode[key] = value;
    }
  }

  // 将当前生成的真实 DOM 元素添加到容器中
  container.appendChild(domNode);
}


4. 简化版 useState 实现

  • 每次渲染时,useState 返回当前组件的状态,并在状态更新后重新渲染视图
let componentsState = [];  // 存储所有组件的状态
let currentComponentIndex = 0;  // 当前组件的索引

function useState(initialValue) {
  // 获取当前组件的状态,若没有则使用初始值
  const componentState = componentsState[currentComponentIndex] || { state: initialValue };

  function setState(newValue) {
    // 更新状态并触发重新渲染
    componentState.state = newValue;
    componentsState[currentComponentIndex] = componentState;
    renderApp();  // 重新渲染组件
  }

  componentsState[currentComponentIndex] = componentState;  // 存储状态
  currentComponentIndex++;  // 更新索引,确保状态独立

  return [componentState.state, setState];  // 返回状态和更新函数
}

5. 函数组件

React 中的组件是函数式的,每个组件都可以接收 props,并返回一个虚拟 DOM。

function MyComponent(props) {
  const [count, setCount] = useState(0);

  return createElement("div", null,
    createElement("h1", null, `Count: ${count}`),
    createElement("button", { onclick: () => setCount(count + 1) }, "Increment")
  );
}

6. 渲染应用

我们使用 renderApp 来启动和更新应用。每次组件状态更新时,renderApp 会触发整个页面的重新渲染。


function renderApp() {
  currentComponentIndex = 0;  // 每次重新渲染时重置组件索引
  const vdom = MyComponent();
  const container = document.getElementById("root");
  container.innerHTML = ''; // 清空之前的渲染
  render(vdom, container);
}

7. 完整的 HTML 文件

完整的 HTML 文件如下所示:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My React</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        padding: 20px;
      }
      button {
        padding: 8px 12px;
        font-size: 16px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>

    <script>
      function createElement(type, props, ...children) {
        return {
          type,
          props: {
            ...props,
            children: children.flat()
          }
        };
      }

      function render(vdom, container) {
        if (typeof vdom === 'string') {
          container.appendChild(document.createTextNode(vdom));
          return;
        }

        const domNode = document.createElement(vdom.type);

        for (const [key, value] of Object.entries(vdom.props)) {
          if (key === 'children') {
            value.forEach(child => render(child, domNode));
          } else {
            domNode[key] = value;
          }
        }

        container.appendChild(domNode);
      }

      let componentsState = [];
      let currentComponentIndex = 0;

      function useState(initialValue) {
        const componentState = componentsState[currentComponentIndex] || { state: initialValue };

        function setState(newValue) {
          componentState.state = newValue;
          componentsState[currentComponentIndex] = componentState;
          renderApp();
        }

        componentsState[currentComponentIndex] = componentState;
        currentComponentIndex++;

        return [componentState.state, setState];
      }

      function MyComponent(props) {
        const [count, setCount] = useState(0);

        return createElement(
          'div',
          null,
          createElement('h1', null, `次数: ${count}`),
          createElement('button', { onclick: () => setCount(count + 1) }, '点击')
        );
      }

      function renderApp() {
        currentComponentIndex = 0;
        const vdom = MyComponent();
        const container = document.getElementById('root');
        container.innerHTML = '';
        render(vdom, container);
      }

      renderApp();
    </script>
  </body>
</html>