mini-react
300行搭建一个自己的mini-react
- jsx
- requestIdlcallback
- fiber
- diff
- useState
- useEffect
贴上 github-repo https://github.com/lidelong-0201/mini-react
jsx
首先看一下jsx,jsx是React.createElement的语法糖,我们平时写jsx时会被webpack脚手架的@babel/preset-react或vite脚手架的@vitejs/plugin-vue-jsx** **等进行转换,(转换配置可以看一下,之前实现搭建webpack的代码库:repo)
<div id='app'>app</div>
//会被处理成
React.createElemnt('div',{id='app'},'app')
例如,我在程序中写了一个打印foo的log,处理之后的打印是这样的
来实现React.createElemnt与createTextNode
下面贴上实现代码
function createTextNode(nodeValue) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue,
children: []
}
};
}
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children?.map((child) => {
const isTextNode = ['string', 'number'].includes(typeof child);
return isTextNode ? createTextNode(child) : child;
})
}
};
}
fiber
fiber的能力
- 增量渲染(把渲染任务拆分成块,匀到多帧)
- 更新时能够暂停,终止渲染任务
- 并发更新
{
parent: fiber,
props: child.props,
type: child.type,
sibling: null,
dom: oldFiberChild.dom,
child: null,
altemate: oldFiberChild,
effectTag: 'UPDATE'|'PLACEMENT',
effectHooks:{
callback,
deps, // 依赖项
cleanup: //callback的void,
}[]
stateHooks:{state:any,queue:action}[]
}
requestIdleCallback
问题
当有大量dom需要同时渲染的时候,直接去渲染会造成浏览器卡顿被用户所感知
分析问题
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。 浏览器每一帧都需要完成
- 处理用户的交互
- JS 解析执行
- 帧开始。窗口尺寸变更,页面滚去等的处理
- rAF
- 布局
- 绘制
解决问题
构建workCallback 函数是一个工作回调函数,它接收一个 deadLine 参数。在函数内部,我们使用一个 while 循环来执行工作单元的任务,直到达到 shouldYield 的条件或者没有下一个工作单元为止。如果 wipRoot 的兄弟节点的类型与 nextWorkOfUnit 的类型相同,我们将 nextWorkOfUnit 设为 null。shouldYield 的值是根据 deadLine.timeRemaining() 是否小于 1 来判断的。如果满足条件,即剩余时间小于 1,shouldYield 将设为 true。 如果 nextWorkOfUnit 为 null 且 wipRoot 不为 null,则执行 commitRoot(),进行渲染操作。 最后,我们使用 window.requestIdleCallback(workCallback) 来请求下一个空闲时间段执行 workCallback。
具体实现代码
const workCallback = (deadLine) => {
let shouldYield = false
while (!shouldYield && nextWorkOfUnit) {
nextWorkOfUnit = perFormWorkOfUnit(nextWorkOfUnit)
if (wipRoot?.sibling?.type === nextWorkOfUnit?.type) {
nextWorkOfUnit = null
}
shouldYield = deadLine.timeRemaining() < 1
}
// render 时条件满足不会执行多变
if (!nextWorkOfUnit && wipRoot) {
commitRoot()
}
window.requestIdleCallback(workCallback)
Diff
在 React 进行双树对比时,会根据 alternate 属性来找到当前的 Fiber 节点。
目前实现了组件 diff 和元素 diff。
在对比组件时,判断是否为同一个组件。如果不是同一个组件,会在调和节点上打上 effectTag 标签,以进行删除并重新创建节点。
在对比元素时,判断当前属性 props 是否相同,是否需要更新。如果需要更新,同样在调和节点上打上更新标签。如果判断出不同的 DOM 标签,例如从 <div>
变成了 <p>
,则进行删除并重新创建。
useState
实现useState 需要在fiber上构建stateHooks,通过index来判断是那个state,最后在统一提交节点进行更新
// 每次更新处理function 会重制为【】
let stateHooks = []
let stateIndex = 0
const useState = (initVal) => {
let currentFiber = wipFunctionFiber
const oldHook = currentFiber.altemate?.stateHooks[stateIndex]
const stateHook = {
state: oldHook ? oldHook.state : initVal,
// 任务队列 收集统一执行
queue: oldHook ? oldHook.queue : [],
}
stateHooks.push(stateHook)
stateIndex++
currentFiber.stateHooks = stateHooks
// 每次执行React。useState 统一执行action
stateHook.queue.forEach((item) => {
stateHook.state = item
})
const setState = (val) => {
// 值相同不更新
if (val === stateHook.state) return
stateHook.queue.push(val)
// stateHook.state = val
wipRoot = {
...currentFiber,
altemate: currentFiber,
}
nextWorkOfUnit = wipRoot
}
return [stateHook.state, setState]
}