之前学习react就是简单的看了看文档,就学习了几个hook的使用。然后就是上手项目,面试中经常会问到react的一些设计原理(虽然没几个面试),手写一个mini-react有利于我们去理解,写下这篇文章以此记录。
开始
首先,需要了解React渲染大概分为三个阶段:
- beginWork,开始处理,将JSX转化成React Element
- reconcile,调和阶段,将React Element转化成Fiber结构,形成Fiber链表
- commit,提交,将Fiber转化成真实DOM,呈现在页面上,并且处理一些useEffect传入的回调
首先是创建React Element,我感觉应该就是常说的Vnode。JSX经过bable编译成以下形式:
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;
})
.flat(Infinity), // 这里是因为map传入的数组会导致形成一个二维数组,所以粗暴的使用flat展开了。。
},
};
}
其中的MiniReact是我们自定义的JSX工厂函数,可以配置tsconfig文件里的jsxFactory属性,默认是React.createElement
从render函数出发,传入我们的入口组件,一般是App,以及需要挂载的容器。一般项目里都有个index.html的文件,里面有一个id为app或者root的div。
看下render的实现,wipRoot就是当前处理的树,React采用了双缓存树的结构,wipRoot就是根节点,也就是当前处理的树的根节点,与之对应的是currentRoot,表示当前在页面展示的树的根节点。两棵树,使用alternate进行关联,下面的代码中就表示了wipRoot使用alternate指向了currentRoot。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot;
}
然后开启我们的渲染工作work loop,由于页面的内容是由 JS 动态生成的,JS的执行和浏览器渲染进程是互斥的,所以react使用分片,将JS的执行分为一个个任务,在浏览器每一帧渲染后的空余时间来执行这些任务,这样既可以提升性能,也不会因为JS执行时间过长导致的阻塞,而且依据fiber的设计实现了可中断渲染的特性,随时可以从中断的的地方继续,因为fiber保存了父节点,兄弟节点,子节点的信息。
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
这里使用了requestIdleCallback来实现上述讲到的利用空闲时间来执行JS。
在performUnitOfWork中,我们主要是针对fiber的类型做不同的处理,是原生元素,还是函数组件或者类组件这样,然后处理完之后就返回child,没有就返回sibling,否则就是向上找父节点,这里return代表父节点
/** 处理fiber,工作单元*/
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function;
// 根据type来处理不同的fiber节点
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
// 处理完成后,先处理child
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
// 如果child节点不存在,那么优先处理sibling节点,也就是兄弟节点
// 兄弟节点也没有就回到return代表父节点,向上找父节点处理sibling节点
// 按照child,sibling,return(parent)顺序遍历处理,串成链表结构
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}
如果当前fiber节点的类型是函数组件的话,就会传入props调用函数形成拿到Vnode,然后进入调和将其转化成fiber
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, fiber.props.children);
原生元素就会创建按DOM,然后计入调和处理children
if (!fiber.dom) {
fiber.dom = createDom(fiber); // 判断是否是文本节点,创建DOM,然后进行一个更新操作,重新绑定事件以及属性
}
// 进入调和,形成fiber
reconcileChildren(fiber, fiber.props.children);
在调和阶段,对两个树的新旧元素进行比较,然后给fiber节点打上tag在后续进行增删改的操作
当调和完成后,就是我们的commit阶段,开始处理我们的Fiber树,将其渲染到网页上,也就是渲染真实DOM了,在这个阶段就是处理fiber,根据tag处理fiber节点,然后执行我们传入的副作用函数,也就是传入的useEffect回调以及清理函数等。
总结
React利用时间分片,在每帧空余的时间执行渲染任务,先将jsx编译成ReactElement,然后进入调和阶段将其转化成fiber链表,然后在commit阶段更新到页面上,处理副作用函数等。