实现自己的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.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函数
- 创建一个DOM节点,根据el的类型来决定是创建一个文本节点还是一个元素节点
- 遍历el的props属性,将除了children之外的属性都赋值给dom节点
- 获取el的children属性 遍历children,对每个子元素调用render函数进行递归渲染
- 把子节点添加到父节点中
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 调用方式
- 创建core 文件夹 里面包含React.js和ReactDOM.js两个文件
- 创建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