mini-react的简易版,帮助你面试超神
背景
- 最近参加了mini-react游戏副本,一群人一起完成同样的任务会碰撞出不同火花,看看别人是怎么思考的,参考下。每天完成一个功能并且实现是合理并且很有收获的。通过实际应用场景来思考功能的实现。一小步一小步的完成任务会很有成就感。并且不会有那种一看到的大量代码放弃的想法。
内容
- 我们先是实现了使用vite实现对jsx的支持,之后设计了
render/initChildren等函数来 实现react的fiber,以及简单的任务调度。实现集中commit和函数组件支持function Component之后实现简单的dom更新和children的初步更新,新增和删除update, 最后是useState和useEffect。还有面试问react原理要怎么回答。
总结
1. createRoot和render的初步实现
从最原始的通过使用简单的js的document Api创建 dom,到抽象出虚拟vdom对象 ,再到最后的 jsx语法。
// 固定vdom,操作真实dom。
const createTextNode = (str) => {
return {
type: 'text',
props: {
nodeValue: str,
},
}
}
const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
},
children: children.map((child) => {
const isText = typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean'
return isText ? createTextNode(child) : child
}),
}
}
render使用递归的方式实现dom的创建,当层级非常非常深的时候循环n+可能会出现页面渲染卡顿,原因就是render 的执行时间过长,超过了浏览器一帧一帧渲染视图的时间,我们知道执行js会阻塞dom 渲染,因此会有卡顿问题出现。
2. Fiber
更新的流程有了大概的了解。我理解的fiber是,fiber在整个react架构的整个流程都至关重要。可以说,无论是react的初次渲染还是后续的更新,都是以fiber作为最小任务进行执行。fiber其实就是保存了一个节点在各个执行流程中所需要的各种信息,所代表的真实dom节点,与父/子/兄弟节点之间的关系,更新相关的信息,优先级。
通过 js 的 requestIdleCallback 函数实现,传入一个函数,函数会接收一个 IdleDeadline 参数,他提供了 timeRemaining() 方法用于获取当前执行的空闲时间,利用空闲时间来执行渲染工作
let taskId = 1
function workLoop(deadline) {
taskId++;
let shouldYield = false
while (!shouldYield) {
// run task
console.log(`taskId:${taskId} run task`);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
// 调度器
function workloop(deadline) {
let is = false;
while (!is && nextFiber) {
nextFiber = evenloop(nextFiber);
is = deadline.timeRemaining() < 1;
}
}
requestIdleCallback(workloop);
3. 实现functionComponent
实现Function Component,根据函数生成Dom,并且可以传入props,过程中需要注意,由于function component 本身没有Dom,在寻找父容器和兄弟节点时,需要采用循环向上的方式。
function commitRoot() {
commitWork(fiberRoot.child);
fiberRoot = null;
}
function commitWork(fiber) {
if (!fiber) return;
let fiberParent = fiber.return;
while (!fiberParent.dom) {
fiberParent = fiberParent.return;
}
if (fiber.dom) fiberParent.dom.append(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
initChildren(children, fiber);
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
const dom = (fiber.dom = createDom(fiber.type));
updateProps(dom, fiber.props);
}
const children = fiber.props.children;
initChildren(children, fiber);
}
4. 实现简单的dom更新和children的初步更新,新增和删除
const children = fiber.children || new Array()
let preChild = Object.create(null)
let oldChildFiber = fiber.alternate?.headChildFiber
let childFiber = null
children.forEach((child, index) => {
if (isSameType(child, oldChildFiber)) {
childFiber = {
...child,
dom: oldChildFiber?.dom,
parent: fiber,
headChildFiber: null,
effectTags: 'update',
alternate: oldChildFiber,
}
} else {
childFiber = { ...child, dom: null, sbling: null, parent: fiber, headChildFiber: null }
if (oldChildFiber) {
addDelets(fiber, oldChildFiber)
childFiber.effectTags = 'placement'
}
}
preChild.sbling = index === 0 ? null : childFiber
if (index === 0) fiber.headChildFiber = childFiber
preChild = childFiber
oldChildFiber = oldChildFiber?.sbling
})
while (oldChildFiber) {
addDelets(fiber, oldChildFiber)
oldChildFiber = oldChildFiber.sbling
}
return children
}
4. 实现简单的useState和useEffect
实现 useEffect 存起来 effecthooks 调用时机,在 commitWork 之后执行commitEffectHook 依次遍历找到对应的节点两种情况初始化、依赖项有没有改变。