实现自己的mini-react

实现自己的mini-react

  • 创建运行环境
  • 实现最简单mini-react
    • 渲染dom
    • 封装创建虚拟dom节点
    • 封装函数
    • 封装render函数
    • 对齐react 调用方式
    • 使用 jsx
  • 任务调度器&fiber架构
    • 封装一个workLoop方法
  • 统一提交&实现 function component
    • 统一提交
    • 实现支持 function component
  • 进军 vdom 的更新
    • 实现绑定事件
    • 更新props
  • 击杀 update children
  • 搞定 useState
  • 搞定 useEffect

创建运行环境

pnpm create vite
  • 选择Vanilla创建项目 选择javascript就行
    创建运行环境
  • 删除多余文件 保留最简单目录
    目录

实现最简单mini-react

渲染dom

  • index.html
    <div id="root"></div><script type="module" src="./main.js"></script>
  • main.js代码
	const dom = document.createElement("div");dom.id="app"document.querySelector("#root").appendChild(dom);const text = document.createTextNode("");text.nodeValue = "hello mini react";dom.append(text)

这样就可以在浏览器上看到hello mini react了

封装创建虚拟dom节点

  • 首先抽离节点
const textNode = {type: "TEXT_ELEMENT",props: {nodeValue: "hello mini react",children: []}
}
const el = {type: "div",props: {id: "app",children: [textNode]}
}
  • 渲染dom
	const dom = document.createElement(el.type);dom.id=el.props.iddocument.querySelector("#root").appendChild(dom);const text = document.createTextNode("");text.nodeValue = textNode.props.nodeValue;dom.append(text)

可以看到结果是一样的

封装函数

  • 把上面的el和textNode封装一下方便调用
function createTextNode(nodeValue) {return {type: "TEXT_ELEMENT",props: {nodeValue,children: [],},};
}
function createElement(type, props, ...children) {return {type,props: {...props,children,},};
}
  • 渲染dom
    const textNode=createTextNode("hello mini react");const el = createElement("div",{id:"app"},textNode)const dom = document.createElement(el.type);dom.id=el.props.iddocument.querySelector("#root").appendChild(dom);const text = document.createTextNode("");text.nodeValue = textNode.props.nodeValue;dom.append(text)

可以看到结果是一样的

封装render函数

  1. 创建一个DOM节点,根据el的类型来决定是创建一个文本节点还是一个元素节点
  2. 遍历el的props属性,将除了children之外的属性都赋值给dom节点
  3. 获取el的children属性 遍历children,对每个子元素调用render函数进行递归渲染
  4. 把子节点添加到父节点中
function render(el, container) {// 创建一个DOM节点,根据el的类型来决定是创建一个文本节点还是一个元素节点const dom =el.type === "TEXT_ELEMENT"? document.createTextNode(""): document.createElement(el.type);// 遍历el的props属性,将除了children之外的属性都赋值给dom节点Object.keys(el.props).forEach(key => {if (key !== "children") {dom[key] = el.props[key];}})// 获取el的children属性const children = el.props.children// 遍历children,对每个子元素调用render函数进行递归渲染children.forEach(child => {render(child, dom)})// 将dom添加到container中container.append(dom);
}
  • 重构createElement函数 之前我们传递节点是createTextNode(“hello mini react”) 现在想之间写"hello mini react" 需要修改函数
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child => {return typeof child === "string" ? createTextNode(child) : child}),},};
}
  • 渲染dom
    const el = createElement("div",{id:"app"},"hello mini react")render(el,document.querySelector("#root"))

对齐react 调用方式

  1. 创建core 文件夹 里面包含React.js和ReactDOM.js两个文件
  2. 创建src文件夹 里面包含App.js文件
  • React.js
function createTextNode(nodeValue) {return {type: "TEXT_ELEMENT",props: {nodeValue,children: [],},};
}
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child => {return typeof child === "string" ? createTextNode(child) : child}),},};
}function render(el, container) {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.childrenchildren.forEach(child => {render(child, dom)})container.append(dom);
}
const React={render,createElement
}
export default React
  • ReactDOM.js
import React from './React.js'
const ReactDOM = {createRoot(container) {return {render(App){React.render(App, container)}}}
}export default ReactDOM
  • App.js
import React from '../core/React.js'
const App =<div>Hello mini react! <span>Hi React</span></div>export default App
  • main.js
import App from './src/App.js'
import ReactDOM from './core/ReactDOM.js'
ReactDOM.createRoot(document.querySelector("#root")).render(App)

运行项目发现效果是一样的

使用 jsx

因为刚开始使用vite创建的项目 所以把App.js和main.js改成App.jsx和main.jsx 然后在index.hrml script引用 在运行项目即可

以上就是我们对mini-react的基本搭建

任务调度器&fiber架构

使用了 requestIdleCallback
为什么使用requestIdleCallback
因为 render 函数中执行大量dom 渲染的时候 会导致卡顿,我们需要对任务进行拆分,拆分成一个个小任务,然后依次执行,从而避免卡顿

封装一个workLoop方法

  • React.js
// 工作循环函数
let nextWorkOfUnit = {};
function workLoop(deadline) {// 工作循环函数,用于不断执行任务直至满足条件let shouldDeadline = false; // 初始化一个变量用于判断是否需要满足截止时间while (!shouldDeadline && nextWorkOfUnit) {// 循环执行任务,直到满足截止时间条件或者没有任务可执行nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit); // 使用任务处理函数处理当前任务对象shouldDeadline = deadline.timeRemaining() < 1; // 判断当前任务的截止时间是否小于1}// 请求下一次执行该函数的时间间隔,并递归调用该函数requestIdleCallback(workLoop);
}requestIdleCallback(workLoop);
  • 实现 filber 架构(把树结构转变成链表结构)
  • 首现判断当前子节点(child)中有没有子节点(child)
  • 如果当前节点没有子节点,就找当前节点的兄弟节点(sibling)
  • 如果当前节点没有兄弟节点(sibling),就找当前节点的父节点(parent) 的兄弟节点(sibling)
  • 如果当前节点的父节点(parent) 没有兄弟节点(sibling),就在往上找
  • 如果当前节点没有 parent 那么就结束

结构描述

  • 实现performWorkOfUnit
function createDom(type) {return type === "TEXT_ELEMENT"? document.createTextNode(""): document.createElement(type);
}function updateProps(dom,props){Object.keys(props).forEach((key) => {if (key !== "children") {dom[key] = props[key];}})
}
function initChildren(fiber){const children = fiber.props.children;let prvChild = null;children.forEach((child, index) => {const newFiber = {type: child.type,props: child.props,parent: fiber,child: null,sibling: null,dom: null,};if (index === 0) {fiber.child = newFiber;} else {prvChild.sibling = newFiber;}prvChild = newFiber;});
}
function performWorkOfUnit(fiber) {if (!fiber.dom) {const dom = (fiber.dom =createDom(fiber.type));fiber.parent.dom.append(dom);updateProps(dom,fiber.props)}initChildren(fiber)if (fiber.child) {return fiber.child;}if (fiber.sibling) {return fiber.sibling;}return fiber.parent?.sibling;
}
  • 修改render
function render(el, container) {nextWorkOfUnit = {dom: container,props: {children: [el],},};
}

统一提交&实现 function component

统一提交

我们使用 requestIdleCallback实现任务调度,但是它只有等待浏览器有空闲时间才会执行任务,如果任务很多,那页面渲染就只能看到一半渲染。

  • React.js
let nextWorkOfUnit = {}
let root = null
function render(el, container) {nextWorkOfUnit = {dom: container,props: {children: [el],},};root=nextWorkOfUnit
}
function workLoop(deadline) {let shouldDeadline = false;while (!shouldDeadline && nextWorkOfUnit) {nextWorkOfUnit = sunWorkFun(nextWorkOfUnit);shouldDeadline = deadline.timeRemaining() < 1;}if(!nextWorkOfUnit&&root){commitRoot()}requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);function commitRoot(){commitWork(root.child)
}function commitWork(fiber){if(!fiber) return;if(fiber.dom){fiber.parent.dom.append(fiber.dom);}commitWork(fiber.child)commitWork(fiber.sibling)
}

实现支持 function component

现在我们渲染dom是这样

  • ReactDOM.createRoot(document.querySelector("#root")).render(App)
    我们想改成
  • ReactDOM.createRoot(document.querySelector("#root")).render(<App />)
  • React.js
// 创建元素节点
/*** 创建一个元素* @param {string} type - 元素的类型* @param {Object} props - 元素的属性* @param {...any} children - 元素的子元素* @returns {Object} - 创建的元素对象*/
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map((child) => {/*** 判断子元素是否为文本节点* @type {boolean}*/const isTextNode =typeof child === "string" || typeof child === "number";return isTextNode ? createTextNode(child) : child;}),},};
}
// 提交节点
function commitWork(fiber) {// 检查fiber是否存在if (!fiber) return;// 初始化fiber的父级节点let fiberParent = fiber.parent;// 循环找到有dom节点的父级节点while (!fiberParent.dom) {fiberParent = fiberParent.parent;}fiberParent.dom.append(fiber.dom);// 递归调用commitWork函数处理fiber的子节点commitWork(fiber.child);// 递归调用commitWork函数处理fiber的兄弟节点commitWork(fiber.sibling);
}
/*** 更新函数组件* * @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {const children = [fiber.type(fiber.props)];initChildren(fiber, children);
}function updateHostComponent(fiber) {// 如果fiber没有关联的dom节点if (!fiber.dom) {// 创建一个新的dom节点const dom = (fiber.dom = createDom(fiber.type));// 更新dom节点的属性updateProps(dom, fiber.props, {});}// 获取子元素const children = fiber.props.children;// 初始化子元素initChildren(fiber, children);
}
/*** 函数:performWorkOfUnit* 描述:用于渲染节点的函数* 参数:* - fiber:fiber对象,包含节点的信息*/
function performWorkOfUnit(fiber) {/*** 变量:isFunctionComponent* 类型:boolean* 描述:判断fiber.type是否为函数节点*/const isFunctionComponent = typeof fiber.type === "function";/*** 判断不是函数节点且fiber.dom不存在时,创建dom节点并更新属性*/if (isFunctionComponent) {updateFunctionComponent(fiber);} else {updateHostComponent(fiber);}/*** 判断fiber是否有子节点,返回子节点*/if (fiber.child) {return fiber.child;}/*** 变量:nextFiber* 类型:fiber对象* 描述:遍历fiber对象的父级节点*/let nextFiber = fiber;while (nextFiber) {/*** 判断nextFiber是否有兄弟节点,返回兄弟节点*/if (nextFiber.sibling) return nextFiber.sibling;nextFiber = nextFiber.parent;}
}

进军 vdom 的更新

实现绑定事件

  • App.jsx
function App() {function handleClick(){console.log("🚀 ~ App ~ App:")}return (<div><button onClick={handleClick}>click</button> </div>);
}
  • 修改updateProps函数
function updateProps(dom,props){Object.keys(props).forEach(key=>{if(key.startsWith('on')){// 事件名const eventType = key.slice(2).toLowerCase();// 或.substring(2);dom.addEventListener(eventType,props[key]);}else{dom[key] = props[key];}})
}

更新props

对比 new vdom tree VS old vdom tree,找出差异,更新dom

在这里插入图片描述

  • 创建 update 函数 使用currentRoot变量来存放当前的根节点
let currentRoot=null;
/*** 递归地提交根节点的子节点工作*/
function commitRoot() {commitWork(root.child);currentRoot = rootroot = null
}function update() {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象root = {dom: currentRoot.dom,props: currentRoot.props};root=nextWorkOfUnit
}
  • 找到老的节点
    找到老的节点
function update() {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象root = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot //老的节点};root=nextWorkOfUnit
}
/*** 初始化子fiber对象* @param {Object} fiber - 当前fiber对象* @param {Array} children - 子子fiber对象数组*/
function initChildren(fiber, children) {let prvChild = null; // 上一个子fiber对象let oldFiber = fiber.alternate?.child; // 备用fiber对象children.forEach((child, index) => { // 遍历子fiber对象数组const isSameType = oldFiber && oldFiber.type === child.type; // 判断是否为相同类型let newFiber = null; // 创建新的fiber对象if (isSameType) { // 如果是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: oldFiber.dom, // 设置fiber的dom为备选fiber的domeffectTag: "update", // 设置fiber效果标签为"update"alternate: oldFiber // 设置fiber备选fiber为备选fiber};} else { // 如果不是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: null, // 设置fiber的dom为nulleffectTag: "placement" // 设置fiber效果标签为"placement"};}if (oldFiber) { // 如果备选fiber存在oldFiber = oldFiber.sibling; // 将备选fiber指向下一个fiber}if (index === 0) { // 如果是第一个子fiberfiber.child = newFiber; // 设置当前fiber的子fiber为新fiber} else {prvChild.sibling = newFiber; // 设置前一个子fiber的兄弟fiber为新fiber}prvChild = newFiber; // 更新前一个子fiber为新fiber});
}
  • 比较diff props
  • old 有 new 没有 删除
  • new 有 old 没有 添加
  • old new 都有 更新 2和3 可以合并成一个处理
// 提交节点
function commitWork(fiber) {// 检查fiber是否存在if (!fiber) return;// 初始化fiber的父级节点let fiberParent = fiber.parent;// 循环找到有dom节点的父级节点while (!fiberParent.dom) {fiberParent = fiberParent.parent;}// 如果fiber的effectTag为'update',则更新fiber的dom节点的属性if (fiber.effectTag === 'update') {updateProps(fiber.dom, fiber.props, fiber.alternate?.props)}// 如果fiber的effectTag为'placement',则将fiber的dom节点添加到fiber的父级节点中else if (fiber.effectTag === 'placement') {if (fiber.dom) {fiberParent.dom.append(fiber.dom);}}// 递归调用commitWork函数处理fiber的子节点commitWork(fiber.child);// 递归调用commitWork函数处理fiber的兄弟节点commitWork(fiber.sibling);
}
/*** 更新属性* @param {Object} dom - DOM元素* @param {Object} nextProps - 新的属性* @param {Object} prvProps - 旧的属性*/
function updateProps(dom, nextProps, prvProps) {Object.keys(prvProps).forEach((key) => {if (key !== "children") { // 排除children属性if (!(key in nextProps)) { // 如果新属性中不存在该键dom.removeAttribute(key); // 移除该属性}}});Object.keys(nextProps).forEach((key) => {if (key !== "children") { // 排除children属性if (nextProps[key] !== prvProps[key]) { // 如果新旧属性值不相同if (key.startsWith("on")) { // 如果属性名以"on"开头const eventType = key.slice(2).toLowerCase() // 获取事件类型dom.removeEventListener(eventType, prvProps[key]) // 移除事件监听器dom.addEventListener(eventType, nextProps[key]) // 添加事件监听器} else {dom[key] = nextProps[key]; // 更新属性值}}}});
}
function updateHostComponent(fiber) {// 如果fiber没有关联的dom节点if (!fiber.dom) {// 创建一个新的dom节点const dom = (fiber.dom = createDom(fiber.type));// 更新dom节点的属性updateProps(dom, fiber.props, {});}// 获取子元素const children = fiber.props.children;// 初始化子元素initChildren(fiber, children);
}
  • 变量重命名 为了跟react保持一致性 root改为wipRoot initChildren改为reconcileChildren
  • 更新以下代码
function render(el, container) {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象wipRoot = {dom: container,props: {children: [el],},};nextWorkOfUnit = wipRoot
}
function update() {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象wipRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot};nextWorkOfUnit = wipRoot
}
  • 导出update
  • 查看效果 App.jsx
	import React from "../core/React.js";let count = 10;let props = {id:'container'}function Counter({num}) {function handleClick(){count++;props = {}React.update()}return <div {...props}>Hi React <button onClick={handleClick}>count:{count}</button></div>;}function App(){return <div>Hello mini react!<Counter num={10} /></div>}export default App;

击杀 update children

  • 创建和删除(type不一致的时候 删除旧的 创建新的)
let deletions = [];
let wipFiber = null;
/*** 递归地提交根节点的子节点工作*/
function commitRoot() {deletions.forEach(commitDeletions);commitWork(wipRoot.child);currentRoot = wipRoot;wipRoot = null;deletions = [];
}function commitDeletions(fiber) {if (fiber.dom) {// 初始化fiber的父级节点let fiberParent = fiber.parent;// 循环找到有dom节点的父级节点while (!fiberParent.dom) {fiberParent = fiberParent.parent;}fiberParent.dom.removeChild(fiber.dom);} else {commitDeletions(fiber.child);}
}
/*** 初始化子fiber对象* @param {Object} fiber - 当前fiber对象* @param {Array} children - 子子fiber对象数组*/
function reconcileChildren(fiber, children) {let prvChild = null; // 上一个子fiber对象let oldFiber = fiber.alternate?.child; // 备用fiber对象children.forEach((child, index) => {// 遍历子fiber对象数组const isSameType = oldFiber && oldFiber.type === child.type; // 判断是否为相同类型let newFiber = null; // 创建新的fiber对象if (isSameType) {// 如果是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: oldFiber.dom, // 设置fiber的dom为备选fiber的domeffectTag: "update", // 设置fiber效果标签为"update"alternate: oldFiber, // 设置fiber备选fiber为备选fiber};} else {if (child) {// 如果不是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: null, // 设置fiber的dom为nulleffectTag: "placement", // 设置fiber效果标签为"placement"};}if (oldFiber) {deletions.push(oldFiber); // 将备选fiber添加到删除数组}}if (oldFiber) {// 如果备选fiber存在oldFiber = oldFiber.sibling; // 将备选fiber指向下一个fiber}if (index === 0) {// 如果是第一个子fiberfiber.child = newFiber; // 设置当前fiber的子fiber为新fiber} else {prvChild.sibling = newFiber; // 设置前一个子fiber的兄弟fiber为新fiber}if (newFiber) {prvChild = newFiber; // 更新前一个子fiber为新fiber}});while (oldFiber) {deletions.push(oldFiber); // 将备选fiber添加到删除数组oldFiber = oldFiber.sibling;}
}
// 工作循环函数
function workLoop(deadline) {// 工作循环函数,用于不断执行任务直至满足条件let shouldDeadline = false; // 初始化一个变量用于判断是否需要满足截止时间while (!shouldDeadline && nextWorkOfUnit) {// 循环执行任务,直到满足截止时间条件或者没有任务可执行nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit); // 使用任务处理函数处理当前任务对象if (wipRoot?.sibling?.type === nextWorkOfUnit?.type) {nextWorkOfUnit = undefined;}shouldDeadline = deadline.timeRemaining() < 1; // 判断当前任务的截止时间是否小于1}if (!nextWorkOfUnit && wipRoot) {// 如果没有任务可执行且存在根对象,则提交根对象commitRoot();}// 请求下一次执行该函数的时间间隔,并递归调用该函数requestIdleCallback(workLoop);
}
/*** 更新函数组件** @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {wipFiber = fiber;const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}
function update() {const currentFiber = wipFiber;return () => {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象wipRoot = {...currentFiber,alternate: currentFiber,};nextWorkOfUnit = wipRoot;};
}
  • App.jsx
function Bar() {const update =  React.update()function handleClick(){countBar++;update()}return <div><h1>Foo</h1>{countBar}<button onClick={handleClick}>click</button></div>;
}

搞定 useState

  • 首先经常见到 react更新状态的时候使用useState
  • App.jsx
function Foo() {const [count,setCount] = React.useState(1)function handleClick(){setCount((c)=>c+1)}return <div><h1>Foo</h1>{count}<button onClick={handleClick}>click</button></div>;
}
  • main.js
/*** useState(initialState)函数用于在当前fiber中创建一个新的stateHook。* @param {any} initialState - 初始状态值* @returns {Array} - 返回包含当前状态和setState方法的数组*/
function useState(initialState) {const currentFiber = wipFiber;const oldHooks = currentFiber.alternate?.stateHook;const stateHook = {state: oldHooks ? oldHooks.state : initialState};currentFiber.stateHook = stateHook;/*** setState(actions)函数用于更新stateHook.state。* @param {Function|Object} actions - 更新状态的函数或要更新的状态对象*/function setState(actions) {stateHook.state = actions(stateHook.state)wipRoot = {...currentFiber,alternate: currentFiber,};nextWorkOfUnit = wipRoot;}return [stateHook.state, setState];
}

这样我们在使用上面dom的时候就可以正常使用了 但是还有问题 就是现在只有一个hook 假如有多个的话最后一个执行结果就会替换前面的 所有还需要把之前的存起来 并且批量执行函数

let stateHooks=[];
let stateHookIndex=0;/*** 更新函数组件** @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {stateHooks = [];stateHookIndex = 0;wipFiber = fiber;const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}
/*** useState(initialState)函数用于在当前fiber中创建一个新的stateHook。* @param {any} initialState - 初始状态值* @returns {Array} - 返回包含当前状态和setState方法的数组*/
function useState(initialState) {const currentFiber = wipFiber;const oldHooks = currentFiber.alternate?.stateHooks[stateHookIndex];const stateHook = {state: oldHooks ? oldHooks.state : initialState,queue: oldHooks ? oldHooks.queue : [],};// 执行stateHook.queue中的所有action,更新stateHook.statestateHook.queue.forEach((action) => {stateHook.state = action(stateHook.state);});stateHook.queue = [];stateHookIndex++;stateHooks.push(stateHook);currentFiber.stateHooks = stateHooks;/*** setState(actions)函数用于更新stateHook.state。* @param {Function|Object} actions - 更新状态的函数或要更新的状态对象*/function setState(actions) {// 将action添加到stateHook.queue中stateHook.queue.push(actions);wipRoot = {...currentFiber,alternate: currentFiber,};nextWorkOfUnit = wipRoot;}return [stateHook.state, setState];
}

这样的话就解决了上面的问题 但是假如说我们调用setCount(11)还是有问题 因为我们存入的不是function 所有还需要修改useState

function useState(initialState) {// 省略...function setState(actions) {const isFunction = typeof actions === "function";// 将action添加到stateHook.queue中stateHook.queue.push(isFunction ? actions : () => actions);// 省略...}return [stateHook.state, setState];
}
  • 提前检测 减少不必要的更新 假如说当前值是11,setCount的还是11 这样情况就没必要更新
function useState(initialState) {// 省略...function setState(actions) {const isFunction = typeof actions === "function";const eagerState = isFunction ? actions(stateHook.state) : actions;if (eagerState === stateHook.state) return;// 将action添加到stateHook.queue中stateHook.queue.push(isFunction ? actions : () => actions);// 省略...}return [stateHook.state, setState];
}

搞定 useEffect

  • useEffect 调用的时机是在 React 完成对 DOM 的渲染之后,并且浏览器完成绘制之前
  • useEffect 的第二个参数是依赖数组,不指定的时候副作用指挥在组件渲染后执行一次,如果指定了依赖数组,那么只有当依赖数组中的值发生变化时,副作用才会执行
  • App.jsx
function Foo() {console.log("🚀 ~ Foo ~ Foo:")const [count,setCount] = React.useState(1)function handleClick(){setCount((c)=>c+1)}React.useEffect(()=>{console.log('useEffect: ');},[])return <div><h1>Foo</h1>{count}<button onClick={handleClick}>click</button></div>;
}
  • React.js
/*** 递归地提交根节点的子节点工作*/
function commitRoot() {deletions.forEach(commitDeletions);commitWork(wipRoot.child);commitEffectHooks();currentRoot = wipRoot;wipRoot = null;deletions = [];
}function useEffect(callback, deps) {// 创建一个effectHook对象,包含callback、deps和cleanup属性const effectHook = {callback,deps,cleanup: undefined,};// 将effectHooks赋值给wipFiber的effectHooks属性wipFiber.effectHook = effectHook;
}
function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;fiber.effectHook?.callback()// 递归执行run函数run(fiber.child);run(fiber.sibling);}// 开始执行effectHooks的回调函数run(wipRoot);
}
  • 上面函数初始化的时候就可以监听到了
  • 更新监听
function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;if (!fiber.alternate) {fiber.effectHook?.callback()} else {const oldHooks = fiber.alternate?.effectHook;const needUpdate = oldHooks.deps.some((olDep, idx) => {return olDep !== fiber.effectHook.deps[idx];});needUpdate && fiber.effectHook.callback();}// 递归执行run函数run(fiber.child);run(fiber.sibling);}run(wipRoot);
}

更新已经实现 但是还有问题 他现在只能监听一个 所有监听多个需要修改代码

/*** 更新函数组件** @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {stateHooks = [];stateHookIndex = 0;effectHooks = [];wipFiber = fiber;const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}
let effectHooks = [];
function useEffect(callback, deps) {// 创建一个effectHook对象,包含callback、deps和cleanup属性const effectHook = {callback,deps};// 将effectHook添加到effectHooks数组中effectHooks.push(effectHook);// 将effectHooks赋值给wipFiber的effectHooks属性wipFiber.effectHooks = effectHooks;
}function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;if (!fiber.alternate) {// 初始化effectHooks的cleanup方法fiber.effectHooks?.forEach((hook) => hook.callback());} else {// 更新effectHooks的cleanup方法fiber.effectHooks?.forEach((newHooks, index) => {if (newHooks.deps.length > 0) {const oldHooks = fiber.alternate?.effectHooks[index];const needUpdate = oldHooks.deps.some((olDep, idx) => {return olDep !== newHooks.deps[idx];});needUpdate && newHooks.callback();}});}// 递归执行run函数run(fiber.child);run(fiber.sibling);}// 开始执行effectHooks的回调函数run(wipRoot);
}
  • cleanup 实现
  • cleanup在调用useEffect之前调用 当deps为空的时候不会返回cleanup
  • 作用就是清空副作用
  • App.jsx 表现形式
function Foo() {console.log("🚀 ~ Foo ~ Foo:")const [count,setCount] = React.useState(1)function handleClick(){setCount((c)=>c+1)setBar('bar11')}React.useEffect(()=>{console.log('useEffect: ');return ()=>{console.log('return-cleanup: ');}},[])React.useEffect(()=>{console.log('count: ',count);return ()=>{console.log('return-cleanup: ');}},[count])return <div><h1>Foo</h1>{count}<button onClick={handleClick}>click</button></div>;
}
  • React.js
function useEffect(callback, deps) {// 创建一个effectHook对象,包含callback、deps和cleanup属性const effectHook = {callback,deps,cleanup: undefined,};// 将effectHook添加到effectHooks数组中effectHooks.push(effectHook);// 将effectHooks赋值给wipFiber的effectHooks属性wipFiber.effectHooks = effectHooks;
}
function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;if (!fiber.alternate) {// 初始化effectHooks的cleanup方法fiber.effectHooks?.forEach((hook) => {hook.cleanup = hook.callback();});} else {// 更新effectHooks的cleanup方法fiber.effectHooks?.forEach((newHooks, index) => {if (newHooks.deps.length > 0) {const oldHooks = fiber.alternate?.effectHooks[index];const needUpdate = oldHooks.deps.some((olDep, idx) => {return olDep !== newHooks.deps[idx];});needUpdate && (newHooks.cleanup = newHooks.callback());}});}// 递归执行run函数run(fiber.child);run(fiber.sibling);}// 执行effectHooks的cleanup方法function runCleanup(fiber){// 如果fiber为空,直接返回if (!fiber) return;// 如果fiber的alternate属性存在,且effectHooks属性存在fiber.alternate?.effectHooks?.forEach(hook=>{// 如果hook的deps数组长度大于0if (hook.deps.length > 0) {// 如果hook有cleanup方法,则执行cleanup方法hook.cleanup && hook.cleanup();}})// 递归执行runCleanup方法,传入fiber的child属性作为参数runCleanup(fiber.child);// 递归执行runCleanup方法,传入fiber的sibling属性作为参数runCleanup(fiber.sibling);}// 开始执行effectHooks的回调函数runCleanup(wipRoot);run(wipRoot);
}

到此我们最简单的mini-react 就完成了
文中仅是自己在学习过程的总结,很多地方可能文笔有误,个人思考能力也不足。仅作为个人的学习结课文章,谢谢大家。

github

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/244944.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

源码篇--Redis 五种数据类型

文章目录 前言一、 字符串类型&#xff1a;1.1 字符串的编码格式&#xff1a;1.1.1 raw 编码格式:1.1.2 empstr编码格式:1.1.3 int 编码格式:1.1.4 字符串存储结构展示: 二、 list类型&#xff1a;2.1 List 底层数据支持&#xff1a;2.2 List 源码实现&#xff1a;2.3 List 结构…

canvas绘制欧盟盟旗(European Union Flag)

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

记一次SPI机制导致的BUG定位【不支持:http://javax.xml.XMLConstants/property/accessExternalDTD】

1、前因 今天在生产环境启用了某个功能&#xff0c;结果发现有个文件上传华为云OBS失败了&#xff0c;报错如下&#xff1a; Caused by: java.lang.IllegalArgumentException: 不支持&#xff1a;http://javax.xml.XMLConstants/property/accessExternalDTDat org.apache.xal…

react中数据不可变

先看官网 一、不可变数据的概念 不可变数据意味着数据一旦创建&#xff0c;就不能被更改。在React中&#xff0c;每次对数据的修改都会返回一个新的数据副本&#xff0c;而不会改变原始数据。这种方式确保了数据的稳定性和一致性。 二、Props中的不可变数据 在React中&#xf…

OpenTCS IDEA 全流程搭建和运行指南

OpenTCS IDEA 全流程搭建和运行指南 JDK安装下载JDK版本 openTCS源码下载IDEA 打开运行查看下载源码中gradle版本号下载gradle 二进制文件配置IDEA Gradle本地仓库IDEA打开openTCS项目运行顺序 JDK安装 下载JDK版本 JDK网址 注意&#xff1a; 请下载官方文档标准的java13JDK o…

ubuntu20根目录扩容

ubuntu根目录/ 或者 /home文件夹有时出现空间满了的情况&#xff0c;可以用gparted工具进行空间的重新分配。 首先&#xff0c;如果你是双系统&#xff0c;需要从windows系统下磁盘压缩分配一部分未使用的空间给ubuntu&#xff0c;注意压缩的空间要邻接ubuntu所在盘的位置。 …

图像旋转角度计算并旋转

#!/usr/bin/python3 # -*- coding: utf-8 -*- import cv2 import numpy as np import timedef Rotate(img, angle0.0,fill0):"""旋转:param img:待旋转图像:param angle: 旋转角度:param fill&#xff1a;填充方式&#xff0c;默认0黑色填充:return: img: 旋转后…

纯前端实现了Excel文件转JSON和JSON转Excel下载

需求前提&#xff1a; 上传Excel文件&#xff0c;并将Excel文件的内容拿出来转换为JSON本地定义JSON数据&#xff0c;然后将它封装后转换为Excel文件下载 安装依赖 这两个功能是借助xlsx包实现的&#xff0c;所以需要先安装xlsx包&#xff1a; npm install xlxs依赖引用 i…

【GitHub项目推荐--一款美观的开源社区系统】【转载】

推荐一款开源社区系统&#xff0c;该系统基于主流的 Java Web 技术栈&#xff0c;如果你是一名 Java 新手掌握了基本 JavaEE 框架知识&#xff0c;可以拿本项目作为练手项目。 开源社区系统功能还算完善包含发布帖子、发布评论、私信、系统通知、点赞、关注、搜索、用户设置、…

Android14源码剖析:MediaPlayer与MediaPlayerService区别?(五十四)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

opencv#34 边缘检测(二)

Laplacian(拉普拉斯)算子 前面介绍的Sobel算子和Scharr算子存在的问题: 1.要分别计算两个方向&#xff08;x,y)的边缘&#xff0c;之后将两方向的边缘进行叠加。 2.边缘与方向相关性较大。当我们通过Sobel算子提取x方向检测时&#xff0c;它所能够检测到的边缘都是一个沿着y…

JVM工作原理与实战(二十一):内存管理

专栏导航 JVM工作原理与实战 RabbitMQ入门指南 从零开始了解大数据 目录 专栏导航 前言 一、不同语言的内存管理 1.C/C的内存管理 2.Java的内存管理 二、垃圾回收的对比 1.自动垃圾回收与手动垃圾回收的对比 2.优点与缺点 总结 前言 JVM作为Java程序的运行环境&#…

Spring Boot 3.2.2整合MyBatis-Plus 3.5.5依赖不兼容问题

问题演示 导依赖 当你启动项目就会 抛出该异常 java.lang.IllegalArgumentException: Invalid value type for attribute factoryBeanObjectType: java.lang.String 问题原因 mybatis-plus 中 mybatis 的整合包版本不够导致的 解决方案 排除掉mybatis-plus 中 mybatis 的整合…

51-15 视频理解串讲—TimeSformer论文精读

今天读的论文题目是Is Space-Time Attention All You Need for Video Understanding? Facebook AI提出了一种称为TimeSformer视频理解的新架构&#xff0c;这个架构完全基于transformer&#xff0c;不使用卷积层。它通过分别对视频的时间和空间维度应用自注意力机制&#xff…

【Flink-1.17-教程】-【四】Flink DataStream API(1)源算子(Source)

【Flink-1.17-教程】-【四】Flink DataStream API&#xff08;1&#xff09;源算子&#xff08;Source&#xff09; 1&#xff09;执行环境&#xff08;Execution Environment&#xff09;1.1.创建执行环境1.2.执行模式&#xff08;Execution Mode&#xff09;1.3.触发程序执行…

智能家居20年,从「动手」到「用脑」

【潮汐商业评论/原创】 正在装修新家的Carro最近陷入了纠结之中&#xff0c;“还没想好要怎么装一套完整的智能家居&#xff0c;家里的基装就已经开始了。” 事实上&#xff0c;Carro对智能家居也不了解&#xff0c;并不知道该如何下手&#xff0c;心想“要是能一次性设计好就…

Mysql复习1--理论基础+操作实践--更新中

Mysql 索引索引的分类索引失效sql优化 删除数据库数据恢复 索引InnoDB引擎MyISAM引擎Memory引擎Btree索引支持支持支持hash索引不支持不支持支持R-tree索引不支持支持不支持Full-text索引5.6版本以后支持支持不支持 索引 解释说明: 索引指的是帮助mysql高效的获取数据的结构叫…

list下

文章目录 注意&#xff1a;const迭代器怎么写&#xff1f;运用场合&#xff1f; inserterase析构函数赋值和拷贝构造区别&#xff1f;拷贝构造不能写那个swap,为什么&#xff1f;拷贝构造代码 面试问题什么是迭代器失效&#xff1f;vector、list的区别&#xff1f; 完整代码 注…

4 课程分类查询

4 课程分类查询 4.1 需求分析 下边根据内容管理模块的业务流程&#xff0c;下一步要实现新增课程&#xff0c;在新增课程界面&#xff0c;有三处信息需要选择&#xff0c;如下图&#xff1a; 课程等级、课程类型来源于数据字典表&#xff0c;此部分的信息前端已从系统管理服…

【jetson笔记】vscode远程调试

vscode安装插件 vscode安装远程插件Remote-SSH 安装完毕点击左侧远程资源管理器 打开SSH配置文件 添加如下内容&#xff0c;Hostname为jetson IP&#xff0c;User为登录用户名需替换为自己的 Host aliasHostName 192.168.219.57User jetson配置好点击连接&#xff0c;控制台输…