开场白
在现代前端开发中,React 已成为一种强大的工具,广泛应用于构建高效、可维护的用户界面。作为一名前端开发者,理解 React 的内部机制不仅能帮助你更好地使用它,还能为你提供对其他前端框架和库的深入理解。在这篇文章中,我们将深入探讨如何手写一个简化版的 React,带你体验构建一个微型 React 框架的全过程。
请大佬指教 抱拳了。
React 的基本概念
-
虚拟 DOM: React 使用虚拟 DOM 来表示 UI 的状态,通过比较新旧虚拟 DOM,找出差异,然后最小化地更新真实 DOM。
-
Fiber 架构: React 16 引入的架构,允许将渲染工作分解为多个单元,逐步执行,避免阻塞主线程,提高渲染效率。
创建虚拟 DOM
首先,我们需要实现一个函数来创建虚拟 DOM。虚拟 DOM 是一个描述 UI 结构的普通 JavaScript 对象,它不会直接操作真实 DOM,而是为后续的差异比较提供基础。
function createElement(type, props, ...children) {
const { key, ref, children: propsChildren, ...restProps } = props || {};
if (Array.isArray(propsChildren) && children.length > 0) {
console.error("children和props.children同时存在优先使用children");
}
if (
children.length === 0 &&
Array.isArray(propsChildren) &&
propsChildren.length > 0
) {
children = propsChildren;
}
return {
type,
key: key || null,
ref: ref || null,
props: {
...restProps,
children: children.map((child) => {
if (typeof child === "object") {
return child;
} else {
return createTextElement(child);
}
}),
},
$$typoef: Symbol.for("react.element"),
};
}
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: [],
},
$$typoef: Symbol.for("react.element"),
};
}
-
createElement
: 模仿React.createElement
,用于创建一个包含type
(标签类型)、props
(属性)和children
(子元素)的对象。 -
createTextElement
: 用于处理文本节点,返回一个特定类型的虚拟 DOM 对象。
到此,我们已经有了创建虚拟 DOM 的基础工具。
构建 Fiber 架构
Fiber
会在内存中维护两颗树
- current 用户渲染
- workInProgress 用户diff比对
每次更新时 都会在内存中 diff 比对workInProgress
与新的VDOM
,进而构建workInProgress
完全修改完成workInProgress
后 会将其值赋给current
调用渲染器渲染
let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;
nextUnitOfWork
当前处理的工作单元currentRoot
相当于 Fiber的 currentTree 用于渲染wipRoot
相当于Fiber的另一颗树 workInprogress 用于diff比对deletions
用于存储需要从 DOM 中删除的 Fiber 节点
const render = (container, element) => {
// 构建工作树
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot; // 将当前工作单元设置为根节点
};
- dom 真实dom节点对象
- props 属性值
- props.children 子项列表
- child 第一个子项
- sibling 下一个同级节点
- return 父级节点
- alternate 上一次的老节点
- key
- ref
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
// 没有需要更新的单元 且 有 workInProgress tree 则提交更新 替换树
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
const elements = 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.return;
}
return null;
}
function reconcileChildren(parentFiber, elements) {
let index = 0;
let oldFiber = parentFiber.alternate && parentFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
// 比对类型 类型相同
const sameType = oldFiber && element && element.type === oldFiber.type;
// 类型相同时 更新
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props, // 只换属性
key: oldFiber.key,
dom: oldFiber.dom,
return: parentFiber,
alternate: oldFiber,
effectTag: "UPDATE",
$$typeof: Symbol.for("react.element"),
};
}
// 类型不同且 有新节点时候 新增
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
key: element.key || null,
dom: null,
return: parentFiber,
alternate: null,
effectTag: "PLACEMENT",
$$typeof: Symbol.for("react.element"),
};
}
// 类型不同 且 没有新节点 有旧节点时 删除
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
parentFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
提交替换两颗树
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
let domParentFiber = fiber.return;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.return;
}
const domParent = domParentFiber.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 {
commitDeletion(fiber.child, domParent);
}
}
构建入口
// jsx最后会被babel编译成 React.createElement或jsx形式 该函数返回VDom
const element = createElement(
"div",
{
class: "app",
onClick: () => console.log("clicked"),
},
createElement(
"h1",
{
onClick: (e) => {
e.stopPropagation();
console.log("clicked h1");
},
},
"Hello"
),
createElement("p", null, "React")
);
const container = document.getElementById("root");
render(container, element);
完整代码
结尾
通过手写这个简化版的 React,我们了解了 React 的基本原理和核心机制。希望这次实践能帮助你更好地理解 React,并在实际开发中更高效地使用它。今后,无论遇到什么样的前端挑战,都能以扎实的基础和清晰的思路去应对。继续探索,享受编程的乐趣吧!