实现自己的mini-react

发布时间:2024年01月21日

创建运行环境

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.id
	document.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.id
	document.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.children
    children.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 children

后续更新…

搞定 useState

后续更新…

搞定 useEffect

后续更新…

github

文章来源:https://blog.csdn.net/weixin_41977619/article/details/135684918
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。