# mini-react **Repository Path**: liuzhe163/mini-react ## Basic Information - **Project Name**: mini-react - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-01-16 - **Last Updated**: 2024-01-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # mini-react 崔学社 # mini-react 笔记 ## 第一节:实现最简单 mini-react 目标是:页面上能显示 app, 代码如下: ```js const dom = document.createElement("div"); dom.id = "app"; document.querySelector("#root").append(dom); const textNode = document.createTextNode(""); textNode.nodeValue = "app"; dom.append(textNode); ``` 1. 使用对象描述 el ```js const el = { type: "div", props: { id: "app", children: [ { type: "TEXT_ELEMENT", props: { nodeValue: "app", children: [], }, }, ], }, }; ``` 2. 使用抽离 textElement ```js const textEl = { type: "TEXT_ELEMENT", props: { nodeValue: "app", children: [], }, }; const el = { type: "div", props: { id: "app", children: [textEl], }, }; ``` 3. 使用对象中的属性代替直接赋值 ```js const app = document.createElement(el.type); app.id = el.props.id; document.querySelector("#root").append(app); const textNode = document.createTextNode(""); textNode.nodeValue = textEl.props.nodeValue; app.append(textNode); ``` 4. 动态创建 ```js function createTextNode(text) { return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [], }, }; } function createElement(type, props, ...children) { return { type, props: { ...props, children, }, }; } const textEl = createTextNode("app"); const el = createElement("div", { id: "app" }, textEl); ``` 依旧正常显示 app 5. 创建 render 函数来统一处理 ```js function render(el, container) { // 根据不同类型创建不同的dom const dom = el.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(el.type); // 添加属性 Object.keys(el.props).forEach((key) => { if (key !== "children") { dom[key] = el.props[key]; } }); // 处理子元素 const children = el.props.children; children.forEach((child) => { render(child, dom); }); // 添加到容器中 container.append(dom); } // 使用render const App = createElement("div", { id: "app" }, "app", "---hi mini-react"); console.log(App); render(App, document.querySelector("#root")); ``` 6. 根据 react 的 api 构建核心方法 ```js const ReactDOM = { createRoot(container) { return { render(el) { render(el, container); }, }; }, }; ReactDOM.createRoot(document.querySelector("#root")).render(App); ``` ### 学到 - 数据结构设计的合理可以降低解构的算法复杂度。 ### 引入 jsx 使用 vite 就能直接使用 jsx ## 第二节 实现任务调度器 ### 使用 window.requestIdleCallback ```js // 可以发现每个任务的剩余时间都是不一样的,这就是requestIdleCallback的特点,它会根据当前浏览器的负载情况,来分配时间片,这样就不会影响到用户的交互体验。 function worker(IdleDeadline) { // Description: demo for requestIdleCallback console.log(IdleDeadline.timeRemaining()); } window.requestIdleCallback(worker); ``` ### 改造 react.js 成任务处理 ```js let taskId = 0; function worker(deadline) { taskId++; let shouldYield = false; while (!shouldYield) { // task run 任务运行 console.log(`task id ${taskId}执行`); // dom 渲染 shouldYield = deadline.timeRemaining() < 1; } requestIdleCallback(worker); } requestIdleCallback(worker); ``` ### 把渲染的树转化成可以让任务一个接着一个执行的链表 **转化规则是:** 1. child 2. sibling 3. 叔叔 **initChildren 函数分析:** ```js function initChildren(fiber) { const children = fiber.props.children; let prevChild = null; children.forEach((child, index) => { const newFiber = { type: child.type, props: child.props, child: null, parent: fiber, sibling: null, dom: null, }; if (index === 0) { fiber.child = newFiber; } else { // 每次给当前节点的上一个设置自身为sibling prevChild.sibling = newFiber; } // 每次都把上一个节点赋值给prevChild prevChild = newFiber; }); } ``` 主要工作是遍历 fiber 的 children,把 fiber 的 child 信息完善成包含 parent、sibling、dom 信息的 newFiber。由于 每次执行会记录当前 fiber 为下一个任务的 prevChild,所以在不是第一个索引的时候设置上一个 prevChild 的 sibling 值是当前 newFiber。 ### 在线演示树表转链表演示 [链接](https://pythontutor.com/render.html#mode=edit) ## 第三节实现统一提交和支持 function component > function component 以下简称 FC **上一节代码遗留的问题** 如果执行过程中遇到 requestIdleCallback 没有空余时间,且间隔时间较长(1-2s)后才有空余时间,用户看到的可能是只渲染了几个节点,然后之后才渲染剩余节点。能不能做到统一添加到父节点的。 ### 小步走实现思路: 1. type 的处理 2. 区分 FC 和非 FC 3. 添加到视图的处理 **先试试直接使用**: ```js function Count() { return
count
; } const App = (
mini-react
); ``` 浏览器报错,提供的 tagName 不是一个有效的名称。无法识别 function Count 那就需要处理它:问题出在我们 createDom 的时候没有判断类型属于 function 的情况 在创建 dom 的时候分析,FC 不需要 dom,同时在 initChildren 中 children 是一个数组,由于 FC 是一个函数则需要执行一下得到返回的对象 ```js const isFunctionComponent = typeof fiber.type === "function"; // 如果是不是函数组件,就创建dom if (!isFunctionComponent) { if (!fiber.dom) { const dom = (fiber.dom = createDom(fiber.type)); updateProps(dom, fiber.props); } } // 处理函数组件的children包裹成函数 const children = isFunctionComponent ? [fiber.type()] : fiber.props.children; // 统一使用children参数 initChildren(fiber, children); ``` 当前的 fiber 的父级插入 fiber.dom,由于 function component 没有 dom,所以报错了 ```js fiber.parent.dom.append(fiber.dom); ``` 改造: 需要往上级一直找有 dom 的父级,然后使用 append 把 dom 插入 ```js // 递归调用 function commitWork(fiber) { if (!fiber) return; // 由于FC没有dom,所以需要找到最近的有dom的父级 let parentFiber = fiber.parent; if (!parentFiber.dom) { parentFiber = parentFiber.parent; } // 如果有dom,就添加到父级dom上,不然会添加一个null if (fiber.dom) { parentFiber.dom.append(fiber.dom); } commitWork(fiber.child); commitWork(fiber.sibling); } ``` **但是如果函数组件再嵌套的话还是会找不到 parent 的有效 dom** ```js // 改成循环往上边找 while (!parentFiber.dom) { parentFiber = parentFiber.parent; } ``` ## 第四节 vdom 的更新 ### 实现事件绑定: 在 updateProps 中处理以 on 开头的添加事件绑定 ```js function updateProps(dom, props) { Object.keys(props).forEach((key) => { // 以on开头的是事件 if (key.startsWith("on")) { const eventType = key.slice("2").toLowerCase(); dom.addEventListener(eventType, props[key]); } else { if (key !== "children") { dom[key] = props[key]; } } }); } ``` ## 第五节 diff - 更新 children > 根据类型不同删除旧节点,添加新节点 在 reconcileChildren 中子节点和 oldFiber 对比类型的时候如果是不同类型,且存在 oldFiber 则把旧节点统一储存起来,在 commitWork 的时候统一删除 ```js const isSameType = oldFiber && oldFiber.type === child.type; let newFiber; if (isSameType) { newFiber = { type: child.type, props: child.props, child: null, parent: fiber, sibling: null, dom: oldFiber.dom, effectTag: "update", alternate: oldFiber, }; } else { newFiber = { type: child.type, props: child.props, child: null, parent: fiber, sibling: null, dom: null, effectTag: "placement", // 放行 }; // 这里能找到就说明是update更新的,而不是初始化render if (oldFiber) { console.log("是update"); deletions.push(oldFiber); } } ``` ```js // 统一删除 function commitRoot() { deletions.forEach(commitDeletion); commitWork(wipRoot.child); currentRoot = wipRoot; wipRoot = null; deletions = []; } function commitDeletion(fiber) { if (fiber.dom) { // 如果是FC到这一步的时候 fiber.parent也就是FC就没有dom所以需要再次往上找 let parentFiber = fiber.parent; while (!parentFiber.dom) { parentFiber = parentFiber.parent; } parentFiber.dom.removeChild(fiber.dom); } else { // FC组件的child是真实dom commitDeletion(fiber.child); } } ``` **注意** - FC 类型不一样需要特殊处理 由于 FC 的 child 是真实 dom,所以要再往下删除。但是找父级的时候也需要再次往上找 ### diff-删除多余的老节点 **case 新节点少于旧节点** ### &&节点的 case ```js // 使用 Counter {isBar && bar} ``` ```js // 实现 children.forEach((child, index) => { const isSameType = oldFiber && oldFiber.type === child.type; // 对比当前节点类型和oldFiber类型 一样的话是更新,不一样则放行创建后续处理(现在只处理props的更新) let newFiber; if (isSameType) { newFiber = { type: child.type, props: child.props, child: null, parent: fiber, sibling: null, dom: oldFiber.dom, effectTag: "update", alternate: oldFiber, }; } else { // 确保当前节点不是 false if (child) { newFiber = { type: child.type, props: child.props, child: null, parent: fiber, sibling: null, dom: null, effectTag: "placement", // 放行 }; } if (oldFiber) { deletions.push(oldFiber); } } if (oldFiber) { oldFiber = oldFiber.sibling; } if (index === 0) { fiber.child = newFiber; } else { prevChild.sibling = newFiber; } // 前边创建newFiber的是有如果是false就没有创建,所以如果没有创建则不把当前为false的赋值给prevChild则会在下一个循环到有fiber的时候赋值。相当于跳过false的节点 if (newFiber) { prevChild = newFiber; } }); ``` ## 第六节 实现 useState **要点:** - 将 stateHook 挂载在 fiber 上,fc 组件初始化时按顺序收集,更新时通过 index 获取对应 hook - 收集 action,再下一次更新页面时统一处理 - action 不变,不用触发 fiber 重新渲染 ## 第七节 实现 useEffect > useEffect 的调用时机是在 React 完成对 DOM 的渲染之后,并且在浏览器完成绘制之前。 ### 小步走 1. 先实现对初始化的执行,没有 deps 依赖项 - 里边会用到对 wipRoot 整棵树的递归调用来寻找有 fiber 上有 effectHook 的,进行执行 2. 对有依赖项的执行 - 判断是否是更新或是初始化,使用 fiber 是否存在 alternate 指针是否存在