最近,我通过开发一个名为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的繁琐的增、删、改、查和事件绑定操作。
- 实现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;
})
}
}
}
|
- 将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); // 请求浏览器空闲时执行下一轮工作循环
}
|
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等核心概念。希望这篇文章对你有所帮助!