文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

手写简易版 React 来彻底搞懂 Fiber 架构

2024-12-02 07:44

关注

与其看各种解释,不如手写一个 fiber 版 React,当你能实现的时候,一定是彻底理解了。

vdom 和 fiber

首先,我们用 vdom 来描述界面结构,比如这样:

{
"type": "ul",
"props": {
"className": "list",
"children": [
{
"type": "li",
"props": {
"className": "item",
"children": [
"aa"
]
}
},
{
"type": "li",
"props": {
"className": "item",
"children": [
"bb"
]
}
}
]
}
}

这很明显就是一个 ul、li 的结构。但是我们不会直接手写 vdom,而是会用 jsx:

const data = {
item1: 'bb',
item2: 'cc'
}

const jsx = <ul className="list">
<li className="item" style={{ background: 'blue', color: 'pink' }} onClick={() => alert(2)}>aali>
<li className="item">{data.item1}<i>xxxi>li>
<li className="item">{data.item2}li>
ul>;

jsx 使用 babel 编译,我们配置一下 .babelrc:

module.exports = {
presets: [
[
'@babel/preset-react',
{
pragma: 'Dong.createElement'
}
]
]
}

然后用 babel 编译它:

babel index.js -d ./dist

编译结果是这样的:

const data = {
item1: 'bb',
item2: 'cc'
};
const jsx = Dong.createElement("ul", {
className: "list"
}, Dong.createElement("li", {
className: "item",
style: {
background: 'blue',
color: 'pink'
},
onClick: () => alert(2)
}, "aa"), Dong.createElement("li", {
className: "item"
}, data.item1, Dong.createElement("i", null, "xxx")), Dong.createElement("li", {
className: "item"
}, data.item2));

这里的 createElement 就叫做 render function,它的执行结果是 vdom。

为什么不直接把 jsx 编译为 vdom 呢?

因为 render function 可以执行动态逻辑呀。我们可以加入 state、props,也可以包装一下实现组件。

这样,我们只要实现 Dong.createElement 就能拿到 vdom 了:

createElement 就是返回 type、props、children 的对象。

我们把 children 也放在 props 里,并且文本节点单独创建:

function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
}
}
}

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

const Dong = {
createElement
}

这样执行以后渲染出来的就是 vdom:

我打印了一下:

接下来递归渲染这棵 vdom 不就是渲染么,也就是通过 document.createElement 创建元素、设置属性、样式、事件监听器等。

等等,如果这样做,那就是 React 16 之前的架构了。这个我们实现过:

手写简易前端框架:vdom 渲染和 jsx 编译

手写简易前端框架:function 和 class 组件

手写简易前端框架:vdom 渲染和 jsx 编译

React 16 之后引入了 fiber 架构,就是在这里做了改变,它不是直接渲染 vdom 了,而是先转成 fiber:

本来 vdom 里通过 children 关联父子节点,而 fiber 里面则是通过 child 关联第一个子节点,然后通过 sibling 串联起下一个,所有的节点可以 return 到父节点。

这样不就把一颗 vdom 树,变成了 fiber 链表么?

然后渲染 fiber 就可以了,和渲染 vdom 的时候一样。

为什么费这么多是转成另一种结构再渲染呢?这不是多次一举么?

那肯定不是,fiber 架构的意义在这:

之前我们是递归渲染 vdom 的,然后 diff 下来做 patch 的渲染:



这个渲染和 diff 是递归进行的。

现在变成了这样:

先把 vdom 转 fiber,也就是 reconcile 的过程,因为 fiber 是链表,就可以打断,用 schedule 来空闲时调度(requestIdleCallback)就行,最后全部转完之后,再一次性 render,这个过程叫做 commit。

这样,之前只有 vdom 的 render 和 patch,现在却变成了 vdom 转 fiber 的 reconcile,空闲调度 reconcile 的 scdule,最后把 fiber 渲染的 commit 三个阶段。

意义就在于这个可打断上。因为递归渲染 vdom 可能耗时很多,JS 计算量大了会阻塞渲染,而 fiber 是可打断的,就不会阻塞渲染,而且还会在这个过程中把需要用到的 dom 创建好,做好 diff 来确定是增是删还是改。

dom 有了,增删改也知道了咋做了,一次性 commit 不就很快了么。

这就是 fiber 架构的意义!

接下来我们实现下。

实现 fiber 版 react

我们从上到下来做吧,也就是分别实现 schedule、reconcile、commit

scheduleschdule

就是空闲调度,也就是这样的:

function workLoop(deadline) {
// do xxx
requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

它就是一个不断的循环,就像 event loop 一样,可以叫做 reconcile loop。

然后它做的事情就是 vdom 转 fiber,也就是 reconcile:

我们用两个全局变量来记录当前处理到的 fiber 节点、根 fiber 节点:

let nextFiberReconcileWork = null;
let wipRoot = null;

它做的事情就是循环处理完所有的 reconcile:

let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) {
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
shouldYield = deadline.timeRemaining() < 1;
}

如果有下一个 fiber,并且还有空闲时间,那就执行下一个 vdom 转 fiber 的 renconcile

如果全部都转完了,那就 commit:

if (!nextFiberReconcileWork) {
commitRoot();
}

所以,schedule 的代码就是这样的:

let nextFiberReconcileWork = null;
let wipRoot = null;

function workLoop(deadline) {
let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) {
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
shouldYield = deadline.timeRemaining() < 1;
}

if (!nextFiberReconcileWork) {
commitRoot();
}

requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

每次执行的 performNextWork 就是 reconcile:

function performNextWork(fiber) {

reconcile(fiber);

if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}

reconcile 当前 fiber 节点,然后再按照顺序继续处理 child、sibling,处理完之后回到 return 的 fiber 节点。

这样不断的调度 reconcile。

这就是 schedule 做的事情:schedule 就是通过空闲调度每个 fiber 节点的 reconcile(vdom 转 fiber),全部 reconcile 完了就执行 commit。

接下来实现 reconcile:

reconcile

schdule 的 loop 已经在不断进行了,那么只要提交一个 nextFiberReconcileWork,下次 loop 就能处理到。

所以,这就是 render 的实现:

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
}
}
nextFiberReconcileWork = wipRoot
}

创建根 fiber 节点,赋值给 wipRoot,也就是 working in progress 的 fiber root 的意思。并且下一个处理的 fiber 节点指向它,那么下次 schedule 就会调度这个 fiber 节点,开始 reconcile。

reconcile 是 vdom 转 fiber,但还会做两件事:一个是提前创建对应的 dom 节点,一个是做 diff,确定是增、删还是改。

reconcile 的实现是这样的:

function reconcile(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}

fiber.props.children 就是 vdom 的子节点,这里的 reconcileChildren 就是把之前的 vdom 转成 child、sibling、return 这样串联起来的 fiber 链表:

循环处理每一个 vdom 的 elements,如果 index 是 0,那就是 child 串联,否则是 sibling 串联。创建出的节点都要用 return 指向父节点:

function reconcileChildren(wipFiber, elements) {
let index = 0
let prevSibling = null

while (
index < elements.length
) {
const element = elements[index]
let newFiber = {
type: element.type,
props: element.props,
dom: null,
return: wipFiber,
effectTag: "PLACEMENT",
}

if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}

prevSibling = newFiber
index++
}
}

因为我们只实现渲染,暂时不做 diff 和删除修改,所以这里的 effectTag 都是 placement,也就是新增元素。

通过 schdule 空闲调度这样处理每一个 vdom 转 fiber,就能生成整个 fiber 链表。

所以,这就是 reconcile 做的事情:reconcile 负责 vdom 转 fiber,并且还会准备好要用的 dom 节点、确定好是增、删、还是改,通过 schdule 的调度,最终把整个 vdom 树转成了 fiber 链表。

当 fiber 转完了,那么 schdule 调度就进入到了这里:

if (!nextFiberReconcileWork) {
commitRoot();
}

开始执行 commit:

commit

commit 就是对 dom 的增删改,而且比之前 vdom 架构时的渲染还要快,因为 dom 都提前创建了、也知道是增是删还是改了,那剩下的不就很简单了么?

我们从根 fiber 开始 commit,并且把 wipRoot 设置为空,因为不再需要调度它了:

function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null
}

每个 fiber 节点的渲染就是按照 child、sibling 的顺序以此插入到 dom 中:

function commitWork(fiber) {
if (!fiber) {
return
}

let domParentFiber = fiber.return
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.return
}
const domParent = domParentFiber.dom

if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}

这里每个 fiber 节点都要往上找它的父节点,因为我们只是新增,那么只需要 appendChild 就行。

dom 已经在 reconcile 节点就创建好了,当时我们没细讲,现在来看下 dom 创建逻辑:

function createDom(fiber) {
const dom = fiber.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(fiber.type);

for (const prop in fiber.props) {
setAttribute(dom, prop, fiber.props[prop]);
}

return dom;
}

就是根据类型创建元素,然后设置属性:

属性要分别处理 style、文本节点的 value、事件监听器:

function isEventListenerAttr(key, value) {
return typeof value == 'function' && key.startsWith('on');
}

function isStyleAttr(key, value) {
return key == 'style' && typeof value == 'object';
}

function isPlainAttr(key, value) {
return typeof value != 'object' && typeof value != 'function';
}

const setAttribute = (dom, key, value) => {
if (key === 'children') {
return;
}

if (key === 'nodeValue') {
dom.textContent = value;
} else if (isEventListenerAttr(key, value)) {
const eventType = key.slice(2).toLowerCase();
dom.addEventListener(eventType, value);
} else if (isStyleAttr(key, value)) {
Object.assign(dom.style, value);
} else if (isPlainAttr(key, value)) {
dom.setAttribute(key, value);
}
};

这在 reconcile 时就做好了,commit 自然很快。

这就是 commit 做的事情:把 reconcile 产生的 fiber 链表一次性添加到 dom 中,因为 fiber 对应的节点提前创建好了、是增是删还是改也都知道了,所以,这一个阶段很快。

这样,我们就实现了简易版 React,当然,目前只实现了渲染,我们来试下效果:

这样一段 jsx:

const data = {
item1: 'bb',
item2: 'cc'
}

const jsx = <ul className="list">
<li className="item" style={{ background: 'blue', color: 'pink' }} onClick={() => alert(2)}>aali>
<li className="item">{data.item1}<i>xxxi>li>
<li className="item">{data.item2}li>
ul>;

console.log(JSON.stringify(jsx, null, 4));

Dong.render(jsx, document.getElementById("root"));

渲染以后是这样的:

代码上传到了 github:https://github.com/QuarkGluonPlasma/frontend-framework-exercize

总结

fiber 是 React16 引入的架构变动,为了彻底理解它,我们实现了一个简易版的 fiber 架构的 React。

界面通过 vdom 描述,但是不是直接手写 vdom,而是 jsx 编译产生的 render function 之后以后生成的。这样就可以加上 state、props 和一些动态逻辑,动态产生 vdom。

vdom 生成之后不再是直接渲染,而是先转成 fiber,这个 vdom 转 fiber 的过程叫做 reconcile。

fiber 是一个链表结构,可以打断,这样就可以通过 requestIdleCallback 来空闲调度 reconcile,这样不断的循环,直到处理完所有的 vdom 转 fiber 的 reconcile,就开始 commit,也就是更新到 dom。

reconcile 的过程会提前创建好 dom,还会标记出增删改,那么 commit 阶段就很快了。

从之前递归渲染时做 diff 来确定增删改以及创建 dom,提前到了可打断的 reconcile 阶段,让 commit 变得非常快,这就是 fiber 架构的目的和意义。

当然,我们还没实现 hooks 以及更新删除,后续会陆续实现。

如果想彻底搞懂 fiber 架构,不妨按照文章所写来实现一遍 reconcile 的过程,一定会让你对它有更深的认识。


来源:神光的编程秘籍内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯