mini-react 打卡记录

mini-react 打卡记录

前言

记录一下参加的最近参加的mini-react的训练营打卡活动,每天学习两三个视频然后自己实现视频中的代码,坚持下来发现从中不光学到了这七天的内容,更是学习到了解决问题的思路,以及如何调试、画图捋顺思路,非常感谢这次的打卡机会提高自己的思维和解决问题的能力。

内容

这七天打卡的内容包括以下

  • 实现createRoot的createElement、render方法
  • 实现任务调度器和 fiber架构
  • 实现统一提交和支持 functionComponent
  • 实现事件绑定和更新 props
  • 实现diff更新
  • 实现useState
  • 实现useEffect

实现createRoot的createElement、render方法

实现 React 中的 createElement、render 方法

js 复制代码
ReactDOM.createRoot(document.querySelector("#root")).render(App);

先来复习一下浏览器的document基础知识

首先从写死创建一个 dom 然后渲染到页面上到模拟 vdom,也就 是 js 对象,然后动态创建实现 createElement 和 createTextNode 方法。然后过渡到如何使用JSX的方式实现,这里借助了Vite打包转义的实现。

js 复制代码
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 实现。

js 复制代码
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 代表执行单个任务的函数,并返回下一个要执行的任务单元。

js 复制代码
function workLoop(deadline) {
    let shouldYield = false;

    while (!shouldYield && nextWorkUnit) {
        nextWorkUnit = preformWorkOfUnit(nextWorkUnit);
        shouldYield = deadline.timeRemaining() > 0;
    }
    requestIdleCallback(workLoop);
}
js 复制代码
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 方法在转换完链表结构后统一提交。

js 复制代码
   if (!nextWorkOfUnit && fiberRoot) {
        commitRoot();
    }
js 复制代码
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 区分函数组件和非函数组件场景。

js 复制代码
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的一个目标

  1. 复习 mini-react,结合真实的react源码以及细节并整理详细的内容并输出文章。
  2. 完善代码仓库,提供更好的工程组织结构、README。
  3. 学习单测,并给仓库增加单元测试。
相关推荐
Csvn4 小时前
OpenSpec 详细使用教程
前端
之歆5 小时前
Day19_LESS 完全指南——从入门到工程实践
前端·css·less
云水一下5 小时前
HTML5 从入门到精通:实战收官——从零搭建完整静态网站,综合运用所有知识
前端·html5
不总是5 小时前
Windows 系统 Node.js 免安装版(zip)安装与配置教程(2026 最新)
前端·windows·node.js
冬奇Lab6 小时前
每日一个开源项目(第105篇):Twenty - 跳出 Salesforce 的圈套,定义现代开源 CRM
前端·后端·开源
zhangyao9403306 小时前
开发pc端时,表格的高度怎么设置才能铺满页面
前端·javascript·elementui
kjs--7 小时前
浏览器书签执行脚本
前端
之歆7 小时前
Day16_JavaScript 轮播图与事件工程实战(下篇)
服务器·开发语言·前端·javascript·网络·性能优化
沄媪7 小时前
CSRF 跨站请求伪造
前端·ctf·csrf
kyriewen8 小时前
我关掉了Copilot:因为我写的代码出现在了别人的建议里
前端·javascript·ai编程