一周-每天1 小时,学习实现一个简单的mini-React

关于 从0到1实现一个 mini-React 过程

起因 — 为什么会有这篇文章还得从前几天加入了一个十分”内卷”的群聊开始

崔学社 mini-React.偶然看到朋友圈一叫mini-React 的游戏副本课程,对这觉得这个课程听起来好像挺有意思的,大概看了一下课程内容,觉得很有意思,就果断加入了.然后就开始了一周的”内卷”生活.

过程 — 下面我分享一下这一周”内圈”具体过程和内容, 课程目标是通过每天花1 小时左右,对照 React API 从 0-1,实现基本的功能. 根据我个人的划分难易程度,我会分成 — 3篇文章来写;有兴趣的朋友可以访问我的github github.com/whoelse666/…

———- day01 ———-

当日目标:实现静态js dom 挂载渲染.

  1. 第一天就很基础的 js dom 操作,以及安装 vite 环境实用 jsx 不多讲,直接上代码,需要注意的是 main.js里面 script type=’module’
    代码如下:
/* 
 main.js
*/

function createElement(type, props, children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => {
        if (typeof child === 'string') {
          return createTextNode(child);
        } else {
          return child;
        }
      })
    }
  };
}

function createTextNode(nodeValue) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue
      // children: []
    }
  };
}

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 && children.forEach(child => render(child, dom));
  container.appendChild(dom);
}

const App = createElement('div', { id: 'app' }, ['hello ', 'world']);
const App1 = createTextNode('world');
const root = document.querySelector('#root');
render(App, root);


<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>day01</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="main.js"></script>
</body>
</html>
  1. 安装vite 实用 jsx 通过以下命令创建/运行
npm init vite@latest
yarn create vite  或者  pnpm create vite

cd  project_name
pnpm  i
pnpm dev 

按照以下目录创建对应文件
App.jsx
main.jsx
core/React.js
core/ReactDom.js

一周-每天1 小时,学习实现一个简单的mini-React

———- day02 ———-

当日目标: 实现任务调度器 和 fiber 架构.

利用了 requestIdleCallback API 实现任务调度,这个函数主要作用:函数将在浏览器空闲时期被调用 ” ,防止 dom 很大情况下造成阻塞卡顿;对requestIdleCallback 不清楚的可以 到 查看文档: requestIdleCallback “. fiber这里依旧是模仿 React 使用链表结构将 vDom 进行处理;

代码如下:( index.html 和上面相同)

/* 
React.js
*/

/* 
vdom 结构
vdom: {
  type,
  props:{
  ...more,
  children:[]
  }
}
*/

function createElement(type, props, children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => {
        if (typeof child === "string") {
          return createTextNode(child)
        } else {
          return child
        }
      })
    }
  }
}

function createTextNode(nodeValue) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue
      // children: []
    }
  }
}

 
requestIdleCallback(workLoop)

let nextWorkOfUnit = null

function workLoop(deadline) {
  let shouldYield = false
  const remainingTime = deadline.timeRemaining()
  while (!shouldYield && nextWorkOfUnit) {
    nextWorkOfUnit = performUnitOfWork(nextWorkOfUnit)
    shouldYield = remainingTime < 1
  }


  requestIdleCallback(workLoop)
}

//  vDom 转换成 真实的Dom node 节点
function createDom(type) {
  return type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(type)
}

// 设置 dom props 属性
function setDomProps(dom, props) {
  Object.keys(props).forEach(key => {
    if (key !== "children") {
      dom[key] = props[key]
    }
  })
}

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    //   todo 1. 创建 dom
    const dom = createDom(fiber.type)
    //  todo 2.处理 props
    setDomProps(dom, fiber.props)
    fiber.dom = dom
    fiber.parent.dom.append(dom)
  }

  // todo 3. 转换成链表
  const children = fiber.props.children
  let prevChild = null
  children &&
    children.forEach((child, index) => {
      if (index === 0) {
        // #root 下 第一个dom 是有 dom =( id='app'的 div) 的 dom特殊处理
        // 父子关系互相连接 parent ,child
        fiber.child = child
        child.parent = fiber
      } else {
        // 同一个 fiber 下的 child 满足都在同一级,所以用 sibling链接
        prevChild.sibling = child
        child.parent = prevChild.parent
      }
      prevChild = child
    })
  // todo 4. 处理当前任务后,返回下一个任务
  if (fiber.child) {
    return fiber.child
  }
  if (fiber.sibling) {
    return fiber.sibling
  }
  return fiber.parent?.sibling
}

function render(el, container) {
  nextWorkOfUnit = {
    dom: container,
    props: {
      children: [el]
    }
  }
}
export default {
  render,
  createElement
}


 // ReactDom.js
import React from "./React.js";
const ReactDom = {
  createRoot(container) {
    return {
      render(rootDom) {
        React.render(rootDom, container);
      }
    };
  }
};

export default ReactDom;


//App.jsx

import React from './core/React.js';
const App = React.createElement("div", { id: "app" }, ["hello ", "world"]);

export default App;


//main.jsx

import React from "./core/React.js";
import ReactDOM from "./core/ReactDom.js";
import App from "./App.jsx";

ReactDOM.createRoot(document.getElementById("root")).render(App);

———- day03 ———-

当日目标: 实现 function component 和 dom 挂载/更新的合并提交

在之前的代码中,频繁挂载 dom,引起页面的重绘,回流等问题,性能浪费,在今天
内容中主要是讲 Dom 结构整理完成后,统一一次性挂载到页面根节点;
然后是实现了对

function Counter(){return <div>666</div>}

这样的function 组件渲染的逻辑处理
代码如下:

/* 
React.js
*/
/* 
vdom 结构
vdom: {
type,
props:{
...more,
children:[]
}
}
*/
function createTextNode(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
const isTextNode =
typeof child === "string" || typeof child === "number";
return isTextNode ? createTextNode(child) : child;
}),
},
};
}
function render(el, container) {
nextWorkOfUnit = {
dom: container,
props: {
children: [el],
},
};
root = nextWorkOfUnit;
}
let root = null;
let nextWorkOfUnit = null;
function workLoop(deadline) {
let shouldYield = false;
while (!shouldYield && nextWorkOfUnit) {
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextWorkOfUnit && root) {
commitRoot();
}
requestIdleCallback(workLoop);
}
function commitRoot() {
commitWork(root.child);
root = null;
}
function commitWork(fiber) {
if (!fiber) return;
let fiberParent = fiber.parent;
while (!fiberParent.dom) {
fiberParent = fiberParent.parent;
}
if (fiber.dom) {
fiberParent.dom.append(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
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, 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 {
prevChild.sibling = newFiber;
}
prevChild = newFiber;
});
}
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
initChildren(fiber, children);
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
const dom = (fiber.dom = createDom(fiber.type));
updateProps(dom, fiber.props);
}
const children = fiber.props.children;
initChildren(fiber, children);
}
function performWorkOfUnit(fiber) {
const isFunctionComponent = typeof fiber.type === "function";
if(isFunctionComponent){
updateFunctionComponent(fiber)
}else{
updateHostComponent(fiber)
}
// 4. 返回下一个要执行的任务
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling;
nextFiber = nextFiber.parent;
}
}
requestIdleCallback(workLoop);
const React = {
render,
createElement,
};
export default React;

//App.jsx
import React from "./core/React.js";
function Counter({ num }) {
return <div>count: {num}</div>;
}
function App() {
return (
<div>
hi-mini-react
<Counter num={10}></Counter>
<Counter num={20}></Counter>
</div>
);
}
export default App;

//main.jsx

import React from "./core/React.js";
import ReactDOM from "./core/ReactDom.js";
import App from "./App.jsx";

ReactDOM.createRoot(document.getElementById("root")).render(App);

告一段落

以上内容是我个人觉得,相对比较简单的部分,使用过 React 开发的同学应该问题都不大.,后续我会继续完成后面部分的内容.觉得有用希望可以支持关注,当然也希望各位同学和大佬,可以多多提建议.

Tips 刚开始写这类文章,经验不足,如有不足希望可以留言指正

原文链接:https://juejin.cn/post/7325732421168676916 作者:程序员挖掘机

(0)
上一篇 2024年1月21日 下午4:26
下一篇 2024年1月21日 下午4:36

相关推荐

发表回复

登录后才能评论