mini-react 打卡记录
前言
记录一下参加的最近参加的mini-react的训练营打卡活动,每天学习两三个视频然后自己实现视频中的代码,坚持下来发现从中不光学到了这七天的内容,更是学习到了解决问题的思路,以及如何调试、画图捋顺思路,非常感谢这次的打卡机会提高自己的思维和解决问题的能力。
内容
这七天打卡的内容包括以下
- 实现
createRoot的createElement、render方法
- 实现
任务调度器和 fiber架构
- 实现
统一提交和支持 functionComponent
- 实现
事件绑定和更新 props
- 实现
diff更新
- 实现
useState
- 实现
useEffect
实现createRoot的createElement、render方法
实现 React 中的 createElement、render 方法
ReactDOM.createRoot(document.querySelector("#root")).render(App);
先来复习一下浏览器的document基础知识
首先从写死创建一个 dom 然后渲染到页面上到模拟 vdom,也就 是 js 对象,然后动态创建实现 createElement 和 createTextNode 方法。然后过渡到如何使用JSX的方式实现,这里借助了Vite打包转义的实现。
function render(el, container) {
const dom =
el.type !== "TEXT_ELEMENT"
? document.createElement(el.type)
: document.createTextNode(el.props.nodeValue);
Object.keys(el.props).forEach((key) => {
if (key !== "children") {
dom[key] = el.props[key];
}
});
// 这里处理 children`
const children = el.props.children;
if (children) {
children.forEach((child) => {
render(child, dom);
});
}
container.append(dom);
}
实现任务调度器和 fiber架构
js 是单线程,如果js执行时间过长就会导致页面卡顿,如果dom树渲染的时间过长,就会导致渲染卡顿,所以问题的关键是把每个任务拆分,在浏览器空闲时候去执行task ,这里借助requestIdleCallback 实现。
let taskId = 1
function workLoop(deadline) {
taskId++;
let shouldYield = false
while (!shouldYield) {
// run task
console.log(`taskId:${taskId} run task`);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
接下来我们需要思考的问题就是,既然只有浏览器空闲的时候才去执行任务,那么我们的dom渲染就需要去记录当前渲染的位置,这时候我们想到dom树是树的结构,记录前序涉及到递归,如果我们把树转成链表那么记录前序就是一个很容易的事情了。那么如果实现一个这样的架构呢,react 把这种架构称为 fiber 架构。下面的代码,我们用 nextWorkUnit 记录下一个要执行的任务单元,preformWorkOfUnit 代表执行单个任务的函数,并返回下一个要执行的任务单元。
function workLoop(deadline) {
let shouldYield = false;
while (!shouldYield && nextWorkUnit) {
nextWorkUnit = preformWorkOfUnit(nextWorkUnit);
shouldYield = deadline.timeRemaining() > 0;
}
requestIdleCallback(workLoop);
}
function preformWorkOfUnit(fiber) {
// 1、创建dom 没有值再处理
if (!fiber.dom) {
let dom = (fiber.dom = createDom(fiber.type));
fiber.parent.dom.append(dom);
// 2、处理 props
updateProps(fiber.props, dom);
}
// 3、转换链表 设置好指针
initChild(fiber);
// 4、返回下一个要执行的任务
if (fiber.child) {
return fiber.child;
} else if (fiber.sibling) {
return fiber.sibling;
} else {
return fiber.parent?.sibling;
}
}
实现统一提交和支持 functionComponent
之前的例子我们已经实现了把dom树结构转成链表,并在每个任务中渲染,但是会有一个问题产生,如果浏览器在一段时间内都没有空闲时间,那么用户就会看到渲染部分的dom,用户体验不好,所以在react中是等到执行完毕再统一添加的,也就是统一提交,去掉之前在执行每个任务单元中边转成链表边添加dom的逻辑,使用 commitRoot 方法在转换完链表结构后统一提交。
if (!nextWorkOfUnit && fiberRoot) {
commitRoot();
}
function commitRoot() {
commitWork(fiberRoot.child);
fiberRoot = null;
}
function commitWork(fiber) {
if (!fiber) return;
let fiberParent = fiber.return;
while (!fiberParent.dom) {
fiberParent = fiberParent.return;
}
if (fiber.dom) fiberParent.dom.append(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
综上,其实我们会发现我们在 react 的 render 方法中传递的其实是一个函数组件,但是我们这里传递的其实是一个对象,那么接下来我们需要支持 function component 的形式,通过画图其实可以发现其实转换 function component 的过程其实就是一个开箱的过程,function component 是没有dom 结点的,所以处理的时候会发现有很多case,需要逐个debug,把之前没有考虑到的情况考虑进去。updateFunctionComponent、updateHostComponent 区分函数组件和非函数组件场景。
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
initChildren(children, fiber);
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
const dom = (fiber.dom = createDom(fiber.type));
updateProps(dom, fiber.props);
}
const children = fiber.props.children;
initChildren(children, fiber);
}
未完待续...
写着写着发现需要回顾的内容还是挺多的,决定把文章拆分成两部分,后面的待补全...
后续计划
接下来的TODO如下(也算是2024的一个目标
- 复习 mini-react,结合真实的react源码以及细节并整理详细的内容并输出文章。
- 完善代码仓库,提供更好的工程组织结构、README。
- 学习单测,并给仓库增加单元测试。