Please enable Javascript to view the contents

如何实现一个自己的React

 ·  ☕ 7 分钟

最近,我通过开发一个名为homemade-react的小型React库,实现了React的Fiber结构、useState和useEffect,并使用vite解析JSX。
最终,我成功使用homemade-react实现了一个Todo List应用。
完整代码可以看我的github,https://github.com/Llane00/homemade-react

在这篇文章中,我将分享我对React原理的理解,以及如何实现一个自己的React库。

现在开始我们的homemade-react实现之旅:

从Fiber到DOM

当用户与页面进行交互时,页面的视图会发生改变,而视图的改变实际上就是DOM的改变。React通过Fiber结构对DOM层进行了抽象,实现了局部更新和异步更新,优化了对DOM的繁琐的增、删、改、查和事件绑定操作。

  1. 实现createDom,将vdom创建为DOM节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function createDom(type) {
  // 创建DOM元素的辅助函数
  return type === "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(type);
}

function createTextNode(text) {
  // 创建文本节点的辅助函数
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: []
    }
  }
}

function createElement(type, props, ...children) {
  // 创建React元素的函数
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        const isTextNode = typeof child === 'string' || typeof child === 'number';
        return isTextNode ? createTextNode(child) : child;
      })
    }
  }
}
  1. 将fiber上的props更新到真实的dom上
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function updateProps(domElement, nextProps, prevProps = {}) {
  // 更新DOM元素的属性
  if (!domElement) return;

  // 删除旧属性
  for (const key in prevProps) {
    if (key === "children") continue;
    if (!(key in nextProps)) {
      domElement.removeAttribute(key);
    }
  }

  // 更新或新增新属性
  for (const key in nextProps) {
    if (key === "children") continue;
    if (prevProps[key] === nextProps[key]) continue;

    if (key.startsWith("on")) {
      // 处理事件属性
      const eventName = key.slice(2).toLowerCase();
      domElement.removeEventListener(eventName, prevProps[key]);
      domElement.addEventListener(eventName, nextProps[key]);
    } else {
      // 处理其他属性
      domElement[key] = nextProps[key];
    }
  }
}

实现可中断的workLoop

在早期的React版本中使用了递归的方式来解析和执行渲染树,而递归是无法中断的;
由于JavaScript是单线程的,如果fiber树很大很深递归就会耗费更多的时间,这就导致页面和用户交互时会出现卡顿;
在我自己实现的homemade-react中,使用了requestIdleCallback来在浏览器空闲时执行渲染任务,再结合Fiber让渲染树分割为一个个fiber工作任务点,就可以实现渲染任务的中断和重新执行;

requestIdleCallback会在浏览器空闲时执行workLoop函数,workLoop函数中会开始执行当前任务并给出下一个任务,当前任务结束后会再执行requestIdleCallback,这样就形成了一个循环;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// render函数 需要传入一个app根组件和一个container root dom 节点
function render(element, container) {
  if (typeof element === 'function') {
    element = element();
  };

  workInProcessRootFiber = {
    props: {
      children: [element]
    },
    dom: container,
  }
  nextWorkOfUnit = workInProcessRootFiber;

  requestIdleCallback(workLoop); // 请求浏览器空闲时执行工作循环
}

function workLoop(IdleDeadline) {
  // 工作循环函数
  let shouldYield = false;
  while (nextWorkOfUnit && !shouldYield) {
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit); // 执行单个Fiber节点的工作

    // 如果下一个工作单元的Fiber节点是当前根Fiber节点的兄弟节点,说明当前根Fiber节点的工作已经处理完成
    if (!!nextWorkOfUnit?.type
      && workInProcessRootFiber?.sibling?.type === nextWorkOfUnit?.type
      && workInProcessRootFiber?.sibling.dom === nextWorkOfUnit?.dom
    ) {
      nextWorkOfUnit = undefined;
    }

    shouldYield = IdleDeadline.timeRemaining() < 1; // 是否需要让出处理时间片段
  }

  // 如果没有下一个工作单元且当前根Fiber节点存在,则提交根Fiber节点
  if (!nextWorkOfUnit && workInProcessRootFiber) {
    commitRoot();
  }

  // 如果有下一个工作单元且当前根Fiber节点不存在,则将下一个工作单元设置为根Fiber节点
  if (nextWorkOfUnit && !workInProcessRootFiber) {
    workInProcessRootFiber = nextWorkOfUnit;
  }

  requestIdleCallback(workLoop); // 请求浏览器空闲时执行下一轮工作循环
}

实现performWorkOfUnit函数来处理渲染任务

ok, 现在来看看performWorkOfUnit函数是怎么执行的;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function performWorkOfUnit(fiber) {
  // 区分一下当前的fiber节点是函数组件还是普通组件
  const isFunctionComponent = typeof fiber.type === 'function';

  if (isFunctionComponent) {
    updateFunctionComponent(fiber); // 更新函数组件
  } else {
    updateNormalComponent(fiber); // 更新普通组件
  }

  return getNextWorkOfUnit(fiber); // 获取下一个要处理的Fiber节点
}

function updateFunctionComponent(fiber) {
  // 更新函数组件的Fiber节点
  stateHooks = []; // 重置状态钩子列表
  stateHookIndex = 0; // 重置状态钩子索引
  effectHooks = []; // 重置副作用钩子列表
  workInProcessFiber = fiber; // 设置当前工作中的Fiber节点为函数组件的Fiber节点

  const children = [fiber.type(fiber.props)]; // 调用函数组件获取子元素
  reconcileChildrenFibers(fiber, children); // 协调子Fiber节点
}

function updateNormalComponent(fiber) {
  // 更新普通组件的Fiber节点
  if (!fiber.dom) {
    const domElement = fiber.dom = createDom(fiber.type); // 创建DOM元素
    updateProps(domElement, fiber?.props); // 更新DOM元素的属性
  }
  const children = fiber?.props?.children; // 子元素列表
  reconcileChildrenFibers(fiber, children); // 协调子Fiber节点
}

function getNextWorkOfUnit(fiber) {
  // 获取下一个要处理的Fiber节点
  if (fiber.child) {
    return fiber.child;
  }

  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }

  return null;
}

实现reconcileChildrenFibers函数处理子Fiber节点

Fiber结构中,每个Fiber保存了DOM的属性数据,同时将渲染工作拆分为多个小任务。Fiber树结构使得程序可以一边解析树结构,一边执行渲染工作任务。Fiber让渲染任务可以被切割为多个小任务,并且这些小任务之间可以相互指向。
双缓存的Fiber树使得diff更新更加容易,也减少了页面的闪屏现象。
接下来我们来看看reconcileChildrenFibers函数是怎么实现的;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function reconcileChildrenFibers(parentFiber, children) {
  // 协调子Fiber节点
  const oldParentFiber = parentFiber.alternate; // 旧的父Fiber节点
  let currentOldFiberChild = oldParentFiber?.child; // 当前旧的子Fiber节点
  let prevChild = null; // 上一个子Fiber节点
  children?.forEach((child, index) => {
    const isTheSameType = currentOldFiberChild && (child?.type === currentOldFiberChild?.type);

    let newFiber;
    if (!!child) {
      // 创建新的Fiber节点
      newFiber = {
        type: child.type,
        props: child.props,
        dom: isTheSameType ? currentOldFiberChild.dom : null,
        alternate: isTheSameType ? currentOldFiberChild : null,
        parent: parentFiber,
        child: null,
        sibling: null,
        effectTag: isTheSameType ? 'update' : 'placement',
      }
    }

    if (!isTheSameType && currentOldFiberChild) {
      // 如果类型不同,则需要删除旧的Fiber节点
      fibersNeedDelete.push(currentOldFiberChild);
    }

    if (currentOldFiberChild) {
      currentOldFiberChild = currentOldFiberChild?.sibling;
    }

    if (index === 0) {
      parentFiber.child = newFiber;
    } else {
      if (!!prevChild) {
        prevChild.sibling = newFiber;
      }
    }

    if (newFiber) {
      prevChild = newFiber;
    }
  });

  // 如果旧的Fiber节点还有多余的sibling节点,需要删除
  while (currentOldFiberChild) {
    fibersNeedDelete.push(currentOldFiberChild);
    currentOldFiberChild = currentOldFiberChild?.sibling;
  }
}

实现commitRoot函数提交根Fiber节点

fiber节点树处理完成, 统一渲染到dom树

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function commitRoot() {
  commitDeletions(); // 处理删除节点
  commitWork(workInProcessRootFiber.child); // 处理前端Root Fiber子节点的工作
  commitEffectHooks(); // 处理收集到的副作用钩子
  workInProcessRootFiber = null;
}

function getFiberParentWithDom(fiber) {
  // 获取包含该Fiber节点的父Fiber节点
  let fiberParent = fiber.parent;
  while (!fiberParent.dom) {
    fiberParent = fiberParent?.parent;
  }
  return fiberParent;
}

function commitWork(fiber) {
  // 提交Fiber节点的工作
  if (!fiber) return;

  // 处理新增节点
  if (fiber.effectTag === 'placement' && fiber.dom) {
    const fiberParent = getFiberParentWithDom(fiber); // 获取包含该Fiber节点的父Fiber节点
    if (fiber.dom) {
      fiberParent.dom.append(fiber.dom); // 将DOM元素添加到父节点中
    }
  }

  // 处理更新节点
  if (fiber.effectTag === 'update' && fiber.dom) {
    updateProps(fiber.dom, fiber.props, fiber.alternate.props); // 更新DOM元素的属性
  }

  commitWork(fiber.child); // 递归处理子节点
  commitWork(fiber.sibling); // 递归处理兄弟节点
}

实现useState

在useState中,状态数据存储在对应的组件Fiber上。我理解的是,不同的Fiber代表不同的数据,因此每个Fiber对应一份数据。将数据存储在Fiber上是非常合理的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function useState(initValue) {
  // 自定义useState钩子
  let currentFiber = workInProcessFiber; // 当前工作中的Fiber节点
  const oldHook = currentFiber?.alternate?.stateHooks[stateHookIndex]; // 旧的状态钩子
  const stateHook = {
    state: oldHook ? oldHook.state : initValue, // 状态值
    actionQueue: oldHook ? oldHook.actionQueue : [] // 状态更新队列
  }

  stateHook.actionQueue.forEach((action) => {
    stateHook.state = action(stateHook.state); // 执行状态更新操作
  })
  stateHook.actionQueue = [];

  stateHooks.push(stateHook); // 将状态钩子添加到列表中
  stateHookIndex++;

  currentFiber.stateHooks = stateHooks; // 更新当前工作中的Fiber节点的状态钩子列表

  let setState = (action) => {
    // 先收集action,等到下次调用useState时再统一处理
    stateHook.actionQueue.push(typeof action === 'function' ? action : () => action);

    // 提前去监测一下action的值,如果和当前state一样则不更新
    const eagerState = typeof action === 'function' ? action(stateHook.state) : action;
    if (eagerState === stateHook.state) return;

    workInProcessRootFiber = {
      ...currentFiber,
      alternate: currentFiber,
    };
    nextWorkOfUnit = workInProcessRootFiber;
  };

  return [stateHook.state, setState]; // 返回当前状态值和更新状态的函数
}

实现useEffect

useEffect也是存储在对应的组件Fiber上,当触发Effect事件时,会对新旧Fiber上的依赖项进行对比,以判断是否需要执行Effect。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function commitEffectHooks() {
  // 提交副作用钩子
  function run(fiber) {
    if (!fiber) return;

    const oldFiber = fiber?.alternate;

    // 初始化
    if (!fiber?.alternate) {
      fiber?.effectHooks?.forEach((hook) => {
        hook.cleanup = hook.callback(); // 执行副作用的回调函数,并保存清理函数
      });
    } else {
      // 更新
      fiber?.effectHooks?.forEach((newHook, index) => {
        if (newHook.deps?.length > 0) {
          const oldEffectHook = oldFiber?.effectHooks[index];

          const isDepsChanged = oldEffectHook?.deps.some((oldDep, oldDepIndex) => oldDep !== newHook?.deps[oldDepIndex]);

          isDepsChanged && (newHook.cleanup = newHook.callback()); // 如果依赖发生变化,则执行副作用的回调函数,并保存清理函数
        }
      })
    }

    run(fiber.child);
    run(fiber.sibling);
  }

  function runCleanup(fiber) {
    if (!fiber) return;

    fiber?.alternate?.effectHooks?.forEach((hook) => {
      if (hook?.deps?.length > 0) {
        hook?.cleanup && hook?.cleanup(); // 执行副作用的清理函数
      }
    })

    runCleanup(fiber.child);
    runCleanup(fiber.sibling);
  }

  runCleanup(workInProcessRootFiber);
  run(workInProcessRootFiber);
}

function useEffect(callback, deps) {
  // 自定义useEffect钩子
  const effectHook = {
    callback,
    deps,
    cleanup: null,
  }
  effectHooks.push(effectHook); // 将副作用钩子添加到列表中
  workInProcessFiber.effectHooks = effectHooks; // 更新当前工作中的Fiber节点的副作用钩子列表
}

以上就是关于React原理的简要介绍。通过实现homemade-react,我更深入地理解了React的工作原理,包括Fiber结构、局部更新、异步更新、useState和useEffect等核心概念。希望这篇文章对你有所帮助!

分享

Llane00
作者
Llane00
Web Developer