mini-react 打卡记录

147 阅读4分钟

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的一个目标

  1. 复习 mini-react,结合真实的react源码以及细节并整理详细的内容并输出文章。
  2. 完善代码仓库,提供更好的工程组织结构、README。
  3. 学习单测,并给仓库增加单元测试。