实现一个简单的react

56 阅读9分钟

抛开并发,要实现一个react其实是非常简单的。一个简单的rect通常由如下几部分构成:

  1. render
  2. createElement

通过createElement,来构建虚拟DOM树,例如:我们有如下代码:

function a(){
  return <div classname='' onClick={()=>{}}> <div></div><A/></div>
}

function A(){
  return <div>3333</div>
}

通过babel编译后实际上会变成:

function a() {
  return /*#__PURE__*/React.createElement("div", {
    classname: "",
    onClick: () => {}
  }, " ", /*#__PURE__*/React.createElement("div", null), /*#__PURE__*/React.createElement(A, null), " ");
}
function A() {
  return /*#__PURE__*/React.createElement("div", null, "3333");
}

运行上述代码最后上会形成类似这样的虚拟DOM结构:

image.png

我们暂时只关注:

  1. type
  2. props

1. 实现一个createElement

关于type

type的类型,在我们实现的简单的版本中只有三种

  1. 普通节点类型,例如:divspan等等
  2. 文本节点类型
  3. 函数组件类型

所以我们先写一个createElement:

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  };
}

我们改造一下刚才babel编译的产物:

function a() {
  return createElement("div", {
    classname: "",
    onClick: () => {}
  }, " ",createElement("div", null), createElement(A, null), " ");
}

function A() {
  return createElement("div", null, "3333");
}

直接调用a(),可以看到生成了如下结构 image.png 嗷,可以看到生成了类似的。

2. render函数

接下来写render函数,来遍历这个结构:


function updateComponent(elements, container){}

function updateFunctionComponent(elements, container) {}

function render(elements, container) {
  const { type } = element;

  switch (typeof type) {
    case 'string':
      updateComponent(element, container);
      break;

    case 'function':
      updateFunctionComponent(element, container);
      break;
  }
 }

updateComponent是最终来挂载dom的,而updateFunctionComponent是用来处理type为函数类型的虚拟节点。我们先实现updateComponent,很简单,根据type直接生成dom,然后插入到container就行,之后就是遍历虚拟domchildren属性,一致递归下去:

function updateComponent(element, container) {
  const { type, props } = element;

  let dom = document.createElement(type);

  container.append(dom);

  props.children?.forEach(child => {
    // 挂载子节点
    render(child, dom);
  });
}

至于type函数组件的类型,我们就需要调用type,并且返回调用结果:

function updateFunctionComponent(element, container) {
  const { type, props } = element;
  const children = type(props);

  render(children, container);
}

到此可以得出我们的代码:

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  };
}

function updateFunctionComponent(element, container) {
  const { type, props } = element;
  const children = type(props);

  render(children, container);
}


function updateComponent(element, container) {
  const { type, props } = element;

  let dom = document.createElement(type);

  container.append(dom);

  props.children?.forEach(child => {
    // 挂载子节点
    render(child, dom);
  });
}

function render(element, container) {
  const { type } = element;

  switch (typeof type) {
    case 'string':
      updateComponent(element, container);
      break;

    case 'function':
      updateFunctionComponent(element, container);
      break;
  }
 }

接着我们测试一下:

render(createElement(a,null), document.body)

可以发现插入成功了:

image.png

但是很奇怪,没有"3333":

image.png

这是因为我们没有对文本节点做处理,所以我们需要处理文本节点,只需要构造一个虚拟dom就行,我们改造一下createElement:

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => {
        if (typeof child === 'string') {
          return createTextElement(child);
        } else {
          return child;
        }
      }),
    },
  };
}

/**
 * 创建文本节点
 * @param {*} text
 * @returns
 */
function createTextElement(text) {
  return {
    type: ELEMENT_TYPE.TEXT_ELEMENT,
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

再次运行,可以发现插入成功:

image.png

给出当前完整代码

const ELEMENT_TYPE = {
  TEXT_ELEMENT: 'TEXT_ELEMENT',
};

const isEvent = eventName => eventName.startsWith('on');

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => {
        if (typeof child === 'string') {
          return createTextElement(child);
        } else {
          return child;
        }
      }),
    },
  };
}

/**
 * 创建文本节点
 * @param {*} text
 * @returns
 */
function createTextElement(text) {
  return {
    type: ELEMENT_TYPE.TEXT_ELEMENT,
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

function updateComponent(element, container) {
  const { type, props } = element;

  let dom = null;

  if (type === ELEMENT_TYPE.TEXT_ELEMENT) {
    dom = document.createTextNode(element.props.nodeValue);
  } else {
    dom = document.createElement(type);
  }

  Object.keys(props)
    .filter(prop => prop !== 'children')
    .forEach(prop => {
      if (isEvent(prop)) {
        dom.addEventListener(prop.slice(2).toLowerCase(), props[prop]);
      }
      dom[prop] = props[prop];
    });

  container.append(dom);

  props.children?.forEach(child => {
    render(child, dom);
  });
}

function updateFunctionComponent(element, container) {
  const { type, props } = element;
  const children = type(props);

  render(children, container);
}

/**
 * 渲染器
 * @param {*} element
 * @param {*} container
 */
function render(element, container) {
  const { type } = element;

  switch (typeof type) {
    case 'string':
      updateComponent(element, container);
      break;

    case 'function':
      updateFunctionComponent(element, container);
      break;
  }
}

export { render, createElement };

3.实现并发

接下来我们实现react的并发模式,也就是引入schedulerFiberNodescheduler我们使用requestIdleCallback来代替。

3.1 问题分析

在之前的实现中,我们的渲染过程是同步的,一旦开始渲染,就会一直执行到结束。这会导致两个问题:

  1. 如果渲染树很大,会阻塞主线程,导致页面卡顿
  2. 如果在渲染过程中有高优先级的任务(如用户输入),无法中断渲染来优先处理

React的并发模式就是为了解决这些问题,它的核心思想是:

  • 将渲染工作分解成小单元
  • 可以暂停和恢复渲染
  • 可以为不同的更新分配优先级

3.2 Fiber架构

Fiber是React并发模式的核心,它是一种数据结构,也是一种工作单元。我们先定义Fiber节点:

/**
 * Fiber节点结构
 */
function createFiber(type, props, dom) {
  return {
    type,
    props,
    dom,
    parent: null,
    child: null,
    sibling: null,
    alternate: null,
    effectTag: 'PLACEMENT',
  };
}

Fiber节点包含以下重要属性:

  • type: 与虚拟DOM的type相同
  • props: 与虚拟DOM的props相同
  • dom: 对应的真实DOM节点
  • parent: 父Fiber节点
  • child: 第一个子Fiber节点
  • sibling: 下一个兄弟Fiber节点
  • alternate: 上一次渲染的Fiber节点
  • effectTag: 标记对DOM的操作类型(PLACEMENT, UPDATE, DELETION)

3.3 工作循环

接下来,我们需要实现工作循环,它负责调度和执行工作单元:

// 下一个工作单元
let nextUnitOfWork = null;
// 正在构建的Fiber树(workInProgress树)
let wipRoot = null;
// 上一次提交到DOM的Fiber树
let currentRoot = null;
// 需要删除的节点
let deletions = [];

/**
 * 调度工作循环
 */
function workLoop(deadline) {
  // 是否应该让出控制权
  let shouldYield = false;
  
  // 如果有下一个工作单元且不需要让出控制权
  while (nextUnitOfWork && !shouldYield) {
    // 执行当前工作单元并返回下一个工作单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // 检查是否还有剩余时间
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  // 如果没有下一个工作单元且有根Fiber,提交整个Fiber树
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  
  // 继续请求下一次空闲回调
  requestIdleCallback(workLoop);
}

// 开始第一次空闲回调
requestIdleCallback(workLoop);

3.4 执行工作单元

每个工作单元的执行过程如下:

/**
 * 执行工作单元
 * @param {*} fiber 当前Fiber节点
 * @returns 下一个工作单元
 */
function performUnitOfWork(fiber) {
  // 处理当前Fiber节点
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  
  // 创建子Fiber节点
  const elements = fiber.type === 'function' 
    ? fiber.type(fiber.props) 
    : fiber.props.children;
  
  reconcileChildren(fiber, elements);
  
  // 返回下一个工作单元
  // 优先处理子节点
  if (fiber.child) {
    return fiber.child;
  }
  
  // 没有子节点则处理兄弟节点
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    // 没有兄弟节点则回到父节点
    nextFiber = nextFiber.parent;
  }
}

/**
 * 创建DOM节点
 */
function createDom(fiber) {
  const dom = 
    fiber.type === ELEMENT_TYPE.TEXT_ELEMENT
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  
  // 设置属性
  updateDom(dom, {}, fiber.props);
  
  return dom;
}

/**
 * 协调子节点
 */
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;
  
  // 遍历子元素,创建子Fiber
  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",
      };
    }
    
    // 有旧Fiber但没有新元素,执行删除
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }
    
    // 移动旧Fiber指针
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }
    
    // 将新Fiber添加到Fiber树中
    if (index === 0) {
      wipFiber.child = newFiber;
    } else if (element) {
      prevSibling.sibling = newFiber;
    }
    
    prevSibling = newFiber;
    index++;
  }
}

3.5 提交阶段

在工作循环中,我们将DOM操作分离到提交阶段,这样可以避免用户看到不完整的UI:

/**
 * 提交整个Fiber树到DOM
 */
function commitRoot() {
  // 先处理需要删除的节点
  deletions.forEach(commitWork);
  // 提交wipRoot的子树
  commitWork(wipRoot.child);
  // 保存当前Fiber树,用于下次比较
  currentRoot = wipRoot;
  // 清空wipRoot
  wipRoot = null;
}

/**
 * 提交单个Fiber节点到DOM
 */
function commitWork(fiber) {
  if (!fiber) {
    return;
  }
  
  // 找到最近的有DOM的父Fiber
  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;
  
  // 根据effectTag执行相应的DOM操作
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent);
  }
  
  // 递归提交子节点和兄弟节点
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

/**
 * 处理节点删除
 */
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    // 如果当前Fiber没有DOM节点(如函数组件),递归查找子节点
    commitDeletion(fiber.child, domParent);
  }
}

/**
 * 更新DOM属性
 */
function updateDom(dom, prevProps, nextProps) {
  // 移除旧属性
  Object.keys(prevProps)
    .filter(key => key !== "children")
    .filter(key => !(key in nextProps))
    .forEach(key => {
      // 移除事件监听器
      if (key.startsWith("on")) {
        const eventType = key.toLowerCase().substring(2);
        dom.removeEventListener(eventType, prevProps[key]);
      } else {
        dom[key] = "";
      }
    });
  
  // 设置新属性
  Object.keys(nextProps)
    .filter(key => key !== "children")
    .forEach(key => {
      // 添加事件监听器
      if (key.startsWith("on")) {
        const eventType = key.toLowerCase().substring(2);
        // 移除旧的事件监听器
        if (prevProps[key]) {
          dom.removeEventListener(eventType, prevProps[key]);
        }
        dom.addEventListener(eventType, nextProps[key]);
      } else {
        dom[key] = nextProps[key];
      }
    });
}

3.6 重新设计render函数

现在我们需要重新设计render函数,使其适配Fiber架构:

/**
 * 渲染函数
 */
function render(element, container) {
  // 设置wipRoot为新的Fiber根节点
  wipRoot = {
    type: "div",
    props: {
      children: [element],
    },
    dom: container,
    alternate: currentRoot,
    effectTag: "UPDATE",
  };
  
  deletions = [];
  nextUnitOfWork = wipRoot;
}

4. 实现Hooks

React的另一个重要特性是Hooks,它允许在函数组件中使用状态和其他React特性。我们来实现最基本的useState

4.1 状态管理

首先,我们需要一些全局变量来跟踪hooks:

// 当前正在处理的函数组件对应的Fiber
let wipFiber = null;
// 当前hook的索引
let hookIndex = null;

4.2 处理函数组件

我们需要修改处理函数组件的逻辑:

/**
 * 处理函数组件
 */
function updateFunctionComponent(fiber) {
  // 设置wipFiber和重置hookIndex
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];
  
  // 执行函数组件,获取子元素
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

4.3 实现useState

最后,实现useState钩子:

/**
 * useState钩子
 */
function useState(initial) {
  // 获取旧的hook
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  
  // 创建新的hook
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],
  };
  
  // 执行上一次渲染后入队的所有action
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach(action => {
    hook.state = action(hook.state);
  });
  
  // 设置状态更新函数
  const setState = action => {
    hook.queue.push(typeof action === "function" ? action : () => action);
    
    // 触发重新渲染
    wipRoot = {
      type: currentRoot.type,
      props: currentRoot.props,
      dom: currentRoot.dom,
      alternate: currentRoot,
      effectTag: "UPDATE",
    };
    
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  
  // 保存hook
  wipFiber.hooks.push(hook);
  hookIndex++;
  
  return [hook.state, setState];
}

5. 完整代码

将所有代码整合在一起,我们得到了一个简单但功能完整的React实现:

const ELEMENT_TYPE = {
  TEXT_ELEMENT: 'TEXT_ELEMENT',
};

// 全局变量
let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;
let deletions = [];
let wipFiber = null;
let hookIndex = null;

/**
 * 创建元素
 */
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => {
        if (typeof child === 'string' || typeof child === 'number') {
          return createTextElement(child);
        } else {
          return child;
        }
      }).filter(Boolean),
    },
  };
}

/**
 * 创建文本元素
 */
function createTextElement(text) {
  return {
    type: ELEMENT_TYPE.TEXT_ELEMENT,
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

/**
 * 渲染函数
 */
function render(element, container) {
  wipRoot = {
    type: "div",
    props: {
      children: [element],
    },
    dom: container,
    alternate: currentRoot,
    effectTag: "UPDATE",
  };
  
  deletions = [];
  nextUnitOfWork = wipRoot;
}

/**
 * 工作循环
 */
function workLoop(deadline) {
  let shouldYield = false;
  
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

/**
 * 执行工作单元
 */
function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  
  if (isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }
  
  // 返回下一个工作单元
  if (fiber.child) {
    return fiber.child;
  }
  
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

/**
 * 处理函数组件
 */
function updateFunctionComponent(fiber) {
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];
  
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

/**
 * 处理宿主组件
 */
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  
  reconcileChildren(fiber, fiber.props.children);
}

/**
 * useState钩子
 */
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 = action(hook.state);
  });
  
  const setState = action => {
    hook.queue.push(typeof action === "function" ? action : () => action);
    
    wipRoot = {
      type: currentRoot.type,
      props: currentRoot.props,
      dom: currentRoot.dom,
      alternate: currentRoot,
      effectTag: "UPDATE",
    };
    
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  
  wipFiber.hooks.push(hook);
  hookIndex++;
  
  return [hook.state, setState];
}

// 导出API
export { createElement, render, useState };

6. 总结

通过这个简单的实现,我们了解了React的核心工作原理:

  1. 虚拟DOM:使用JavaScript对象表示UI结构
  2. 协调算法:比较新旧虚拟DOM树,找出需要更新的部分
  3. Fiber架构:将渲染工作分解成小单元,实现可中断的渲染
  4. 并发模式:利用浏览器空闲时间执行渲染工作,提高用户体验
  5. Hooks:在函数组件中使用状态和其他React特性

这个实现虽然简单,但包含了React的核心思想。实际的React代码要复杂得多,包含了更多的优化和特性,如:

  • 更高效的协调算法
  • 更完善的事件系统
  • 更多的Hooks(useEffect, useContext等)
  • 错误边界
  • 服务端渲染
  • Suspense和并发特性

通过理解这个简化版的实现,我们可以更好地理解React的工作原理,从而更有效地使用React进行开发。