从零实现一个简易版 React:深入理解 JSX、虚拟 DOM 与 Diff 算法

5 阅读2分钟

前言

在现代前端开发中,React 已经成为了最流行的 UI 库之一。但你是否曾好奇过 React 内部是如何工作的?今天,我们将一起动手实现一个简易版的 React,通过这个过程,你将深入理解 JSX 的编译原理、虚拟 DOM 的创建与更新,以及高效的 Diff 算法实现。

一、项目结构与核心概念

首先,让我们明确要实现的几个核心功能:

  1. createElement: 将 JSX 转换为虚拟 DOM 对象
  2. render: 将虚拟 DOM 渲染为真实 DOM
  3. reconcile: 虚拟 DOM 的 Diff 算法
  4. useState: 实现简单的状态管理

让我们从最基础的开始。

二、实现 createElement:JSX 的编译原理

当我们在 React 中编写 JSX 时,Babel 会将其转换为 React.createElement() 调用。让我们自己实现这个函数:

/**
 * 创建虚拟 DOM 元素
 * @param {string} type 元素类型
 * @param {object} props 元素属性
 * @param {...any} children 子元素
 * @returns {object} 虚拟 DOM 对象
 */
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      // 处理 children,文本节点转换为文本元素
      children: children.map(child =>
        typeof child === 'object'
          ? child
          : createTextElement(child)
      ),
    },
  };
}

/**
 * 创建文本虚拟 DOM 元素
 * @param {string} text 文本内容
 * @returns {object} 文本虚拟 DOM 对象
 */
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

为了测试我们的 createElement,我们可以模拟 JSX 的转换过程:

// JSX: <div id="app"><h1>Hello World</h1></div>
const element = createElement(
  'div',
  { id: 'app' },
  createElement('h1', null, 'Hello World')
);

console.log(JSON.stringify(element, null, 2));

三、实现 render:虚拟 DOM 到真实 DOM 的转换

有了虚拟 DOM,接下来我们需要将其渲染到页面上:

/**
 * 将虚拟 DOM 渲染为真实 DOM
 * @param {object} element 虚拟 DOM 元素
 * @param {HTMLElement} container 容器元素
 */
function render(element, container) {
  // 创建 DOM 节点
  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);
}

让我们测试一下渲染功能:

const MiniReact = {
  createElement,
  render,
};

// 模拟 JSX 使用
const App = () => MiniReact.createElement(
  'div',
  { id: 'app', className: 'container' },
  MiniReact.createElement('h1', null, 'Mini React Demo'),
  MiniReact.createElement('p', null, 'This is a paragraph.')
);

// 渲染到页面
const container = document.getElementById('root');
MiniReact.render(App(), container);

四、实现 Concurrent Mode 和 Fiber 架构基础

为了实现更高效的渲染,我们需要引入 Fiber 架构的基本思想:

let nextUnitOfWork = null;
let wipRoot = null; // work in progress root
let currentRoot = null; // 当前渲染的根

/**
 * 开始渲染循环
 * @param {object} element 虚拟 DOM 元素
 * @param {HTMLElement} container 容器元素
 */
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot, // 指向旧的 fiber
  };
  
  nextUnitOfWork = wipRoot;
  
  // 启动工作循环
  requestIdleCallback(workLoop);
}

/**
 * 工作循环,利用空闲时间执行任务
 * @param {IdleDeadline} deadline 空闲时间信息
 */
function workLoop(deadline) {
  let shouldYield = false;
  
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  
  requestIdleCallback(workLoop);
}

/**
 * 执行一个工作单元
 * @param {object} fiber 当前 fiber 节点
 * @returns {object} 下一个工作单元
 */
function performUnitOfWork(fiber) {
  // 1. 创建 DOM 节点
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  
  // 2. 为子元素创建 fiber 节点
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
  
  // 3. 返回下一个工作单元
  if (fiber.child) {
    return fiber.child;
  }
  
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

五、实现 Diff 算法:高效的 DOM 更新

Diff 算法是 React 性能优化的核心。让我们实现一个简化版的 Diff 算法:

/**
 * 协调子元素,实现 Diff 算法
 * @param {object} wipFiber 当前工作的 fiber
 * @param {array} elements 新的子元素数组
 */
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;
  
  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    let newFiber = null;
    
    // 比较新旧 fiber
    const sameType = oldFiber && element && element.type === oldFiber.type;
    
    if (sameType) {
      // 类型相同,更新属性
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: 'UPDATE',
      };
    }
    
    if (element && !sameType) {
      // 类型不同,创建新节点
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: 'PLACEMENT',
      };
    }
    
    if (oldFiber && !sameType) {
      // 删除旧节点
      oldFiber.effectTag = 'DELETION';
      deletions.push(oldFiber);
    }
    
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }
    
    if (index === 0) {
      wipFiber.child = newFiber;
    } else if (element) {
      prevSibling.sibling = newFiber;
    }
    
    prevSibling = newFiber;
    index++;
  }
}

六、实现 useState:简单的状态管理

最后,让我们实现一个简易版的 useState Hook:

let hookIndex = null;
let wipFiber = null;

/**
 * 实现 useState Hook
 * @param {any} initial 初始状态
 * @returns {[any, Function]} 状态和更新函数
 */
function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],
  };
  
  // 处理队列中的更新
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach(action => {
    hook.state = typeof action === 'function'
      ? action(hook.state)
      : action;
  });
  
  const setState = action => {
    hook.queue.push(action);
    
    // 触发重新渲染
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  
  w