页面布局不知道如何选择?来看看Flex和Grid吧

本文逐步介绍了如何构建一个简单的React,从createElement和render函数开始,深入探讨了concurrent mode的原理和实现,以及Fibers在React中的作用。通过实例展示了如何用原生JavaScript实现React的关键功能,包括中断渲染、Fiber树的遍历和协调算法,同时讲解了函数组件和Hooks的使用。最后提到了前端开发中页面布局常用的Flex和Grid布局技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

由浅入深,一步一步构建一个自己的React。

0. 简单说说React干了什么

简单来说,React做的事就是把JSX或者把用React.createElement()函数构建出来的React元素(Symbol(react.element)),用ReactDOM.render()函数渲染到DOM节点中。

如果在控制台打印React.createElement()函数构建的元素。

// React中使用JSX或creatElement
// const element = <h1 title="foo">hello world</h1>;
const element = React.createElement("h1",{ title: "foo" },"Hello world");
console.log(element);
const container = document.getElementById('container');
ReactDOM.render(element, container); 

可以发现React元素是一个含有typeprops属性的对象,其中propschildren(是一个array)属性。

那么用原生JS实现如下:

// 创建element对象
const element = {type: 'h1',props: {title: 'foo',children: 'hello world'}
}
const container = document.getElementById("container")
// 生成DOM节点
const node = document.createElement(element.type)
node["title"] = element.props.title
// 创建文本节点作为children
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
// 把文本节点添加到h1标题,并h1标题添加到container
node.appendChild(text)
container.appendChild(node) 

接下来的内容涉及到很多细节,如concurrent mode,fiber,reconciliation,函数组件等等。下面就一步步用原生JavaScript构建一个myReact。

1. createElement函数

从上节可知,被渲染的React元素是一个对象,所以createElemet函数需要返回至少有typeprops两个关键字的对象。出于简单考虑,我们只用这两个属性就够了。所以createElement 函数实现如下:

// createElement函数
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child =>typeof child === 'object'? child: createPrimitiveElement(child))}}
}

function createPrimitiveElement(text) {return {type: "PRIMITIVE_ELEMENT",props: {nodeValue: primitive,children: []}}
} 

入参为type, props, 和children。其中,children对象用了扩展操作符展开。另外children对象可能含有string或者number等基本数据类型的值,因此单独创建一个createPrimitiveElement函数来构建值为非对象的元素,这里只用用text类型代替所有基本数据类型的值。但是React并不这么干,我们这样做是因为出于简洁实现的考虑。

然后把createElemet函数封装在myReact对象中:

const myReact = {createElement
};
const element = myReact.createElement("div",{ id: "foo" },myReact.createElement("a", null, "bar"),myReact.createElement("b")
);
console.log(element); 

输出结果如下:

2. render函数

从0中可知,render做了生成DOM节点并将节点添加到DOM树中的对应位置。

那么render函数的实现如下:

function render(element, container) {// 创建DOM节点并处理基本数据类型const dom =element.type == "PRIMITIVE_ELEMENT"? document.createTextNode(""): document.createElement(element.type);// 分配元素props给DOM节点const isProperty = key => key !== "children";Object.keys(element.props).filter(isProperty).forEach(name => {dom[name] = element.props[name]});// 对于每个children递归执行render函数element.props.children.forEach(child =>render(child, dom));// 最后把节点添加到对应的DOM节点 container.appendChild(dom);
} 

我们来测试一下:

const myReact = {createElement,render
};

const element = myReact.createElement("div",{ id: "foo" },myReact.createElement("h1", null, "bar"),myReact.createElement("h2", null, "baz"),
);
const container = document.getElementById("container");
myReact.render(element, container); 

3. concurrent mode并发模式

上面的实现问题在哪

问题就在于element.props.children.forEach(child => render(child, dom))这个方法。

这个递归调用是有问题的,一旦开始渲染,就会将所有节点及其子节点全部渲染完成这个进程才会结束。当DOM树很大的情况下,在渲染过程中页面有可能出现卡顿等现象,无法进行用户输入等交互操作。

concurrent mode是什么

concurrent mode是一种可中断渲染的设计架构,解决的是任务拆分任务优先级划定的问题。通常React运行时最大的开销是状态更新到视图变化中间的计算步骤(Fiber Tree和Fiber Reconciler),而优化思路就是减少遍历时需要遍历的Fiber节点数量。concurrent mode做的事情就是根据优先级来进行Fiber的遍历渲染,从而减少遍历的Fiber节点数量。

而Fiber树的更新流程分为render阶段与commit阶段。

  • render 是决定需要渲染什么的阶段,找出需要更新的地方(diff fiber tree),也就是一个计算阶段,计算结果可以被缓存,也可以被打断;
  • commit 是将需要渲染的内容更新到DOM的阶段,也就是提交所有更新并渲染,为了防止页面抖动,被设置为不能被打断。

“可中断渲染”的实现方案是时间分片。cocurrent mode下render阶段可以拆解成一个个时间分片,一个时间分片就是一个渲染帧中js能分配到的最多时间。这个分片的时间和渲染频率有关,肉眼可以接受的流畅画面的最低帧率是60fps,即一帧16.7ms。因此每个时间分片不能大于16.7ms,如果render执行时间大于16.7ms就要需要停止,然后让浏览器先执行渲染(commit)操作,等渲染(commit)空余时候再继续执行render。

即利用浏览器的渲染空余时间来执行render(也可以叫diff)工作,当执行时间过长时候,停止render工作,把执行机会让给渲染(commit),然后等渲染间隙再继续执行render工作,直到render完成,再执行commit渲染。这样不仅避免了渲染工作被js执行阻塞导致的卡顿,还让浏览器有时间对代码进行优化从而提升执行性能。

因此时间分片要求React拥有中断和重启diff的能力。

怎么做

由上文可知,我们需要把工作拆分成时间分片单元,在每个单元执行完后,如果有其他任务需要执行浏览器会打断render。那么我们可用以下步骤解决上述问题:

1.允许中断render阶段的工作,如果有优先级更高的工作插入,则暂时中断浏览器render工作,待完成该工作后,恢复浏览器render工作;2.将render工作进行分解,分解成一个个小单元;3.使用requestIdleCallback来解决允许中断渲染工作的问题。requestIdleCallback将在浏览器的空闲时段内调用排队的函数。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

let nextUnitOfWork = null;

function workLoop(deadline) {let shouldYield = false;while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);shouldYield = deadline.timeRemaining() < 1;};requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(nextUnitOfWork) {// TODO
} 

在这里我们用requestIdleCallback函数来创建一个loop,浏览器会在主线程空闲的时候运行这个loop。但是现在React已经不用requestIdleCallbackAPI了,而是使用scheduler这个package,在 React内部采用requestAnimationFrame进行ployfill,通过 帧率动态调整,计算 timeRemaining,模拟requestIdleCallback,从而实现时间分片。在这里出于代码简洁性的考虑还是用的requestIdleCallback,但基本概念是一样的。

requestIdleCallback传入的是一个IdleDeadline的参数,它提供了一个方法(deadline.timeRemaining()),可以让浏览器判断还剩余多少闲置时间可以用来执行耗时任务。它用来判断在浏览器重新开始执行其他任务之前还剩多长时间。

为了使用这个loop,我们需要设置第一个单元的工作,并且完成这个performUnitOfWork函数,这个函数的功能不仅是执行这个工作单元,而且还返回下一个工作单元。

文档

Introducing Concurrent Mode (Experimental) 这篇文档在一定程度上解释了并发模式,但已经过时了,仅供参考。

What is Concurrent React?这一部分是最新的文档,也供各位学习参考。

4. Fibers

是什么

这里我们说的Fiber是一种新的Reconciler架构,也称为Fiber Reconciler,而在说明新Reconciler之前我们来聊聊旧的Reconciler。

Stack Reconciler

旧的Reconciler(又叫Stack Reconciler) 。React维护一个虚拟DOM(Virtual DOM tree)来映射真正的DOM tree。state的更新会创建一个新的Virtual DOM tree,然后用diffing算法对新旧两个Virtual DOM tree进行对比,从而得到需要修改的节点,然后会一口气将所有的修改更新到真实的DOM。

为什么它叫Stack Reconciler ?因为虚拟DOM是嵌套结构,这里的diff过程是递归 (recursive) 的,因此React团队称React16之前的调度器为栈调度器。

而栈的问题有三个。首先,递归的时候每次创建函数需要生成执行上下文、变量对象,性能消耗较大。其次,递归结构不方便进行中断重启(这是时间分片的实现基础)。比如深度优先遍历,遍历到某个节点时候中断,再重启遍历时候,如果没有复杂的辅助数据结构,是不知道下一个要遍历哪个节点的。最后,递归的时候栈一直被占用,而JavaScript运行环境只有一个call stack,因此造成卡顿的问题。

Fiber Reconciler

出于栈结构的问题,React需要新的架构来支持断点重启。那么Fiber Reconciler应运而生。

首先,虚拟DOM树结构改成了链表结构,因为链表结构利于中断和重启。比如遍历到某个节点时候需要暂停,那么只要记录当前指针,等到重启时候指向下一个就可以了。另外,Fiber架构把递归改成循环(while loop),同时在每次循环结束时做过期(expire)判断,如果过期就标记下当前的位置,然后中断遍历,让主线程有机会完成高优先级的工作。

React设计了fiber tree数据结构,每个fiber tree的node都有3个属性:return(指向父节点)、sibling(指向右兄弟节点)、child(指向第一个子节点)。

上图的箭头表明了 fiber 的渲染过程,渲染过程详细描述如下:

1.从 root 开始,找到第一个子节点 div;
2.找到 div 的第一个子节点 h1;
3.找到 h1 的第一个子节点 p;
4.找 p 的第一个子节点,如无子节点,则找下一个兄弟节点,找到 p 的兄弟节点 a;
5.找 a 的第一个子节点,如无子节点,也无兄弟节点,则找它的父节点的下一个兄弟节点,找到 a 的 父节点的兄弟节点 h2;
6.找 h2 的第一个子节点,找不到,找兄弟节点,找不到,找父节点 div 的兄弟节点,也找不到,继续找 div 的父节点的兄弟节点,找到 root;
7.第 6 步已经找到了 root 节点,渲染已全部完成。

通过上面的分析可以看出,fiber tree并非通过严格的链表来进行遍历,它也是一个树的结构,遍历过程和深度优先遍历一个树没有区别,而区别在于加了三个属性指向相关节点,让遍历可以暂停和重启,很方便地找到一个节点的下一个DFS节点。

fiber可以理解是一种数据结构,是一个树的结构,fiber节点记录的是操作,包括将要进行的操作和已经完成的操作。而fiber架构是包含数据结构和调度机制的一个整体。

怎么做

在上一节中没有实现的performUnitOfWork函数中,我们添加了fiber的实现。

// 原来的render函数改为了createDom函数,在performUnitOfWork中调用
function createDom(fiber) {const dom = fiber.type === 'PRIMITIVE_ELEMENT'? document.createTextNode(""): document.createElement(fiber.type);const isProperty = key => key !== "children";Object.keys(fiber.props).filter(isProperty).forEach(name => {dom[name] = fiber.props[name]});return dom;
}

let nextUnitOfWork = null;

// 将nextUnitOfWork设为fiber tree的根节点
function render(element, container) {nextUnitOfWork = {dom: container,props: {children: [element]}};
}

function workLoop(deadline) {let shouldYield = false;while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);shouldYield = deadline.timeRemaining() < 1;}requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {// 当fiber中没有节点时,创建一个新节点并添加到DOM节点中if (!fiber.dom) {fiber.dom = createDom(fiber);}if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom);}// 对于每个子节点,创建一个新的fiberconst elements = fiber.props.children;let index = 0;let prevSibling = null;while (index < elements.length) {const element = elements[index];const newFiber = {type: element.type,props: element.props,parent: fiber,dom: null,};// 然后把新fiber添加到fiber树中,作为第一个子节点或者左右兄弟节点if (index === 0) {fiber.child = newFiber;} else {prevSibling.sibling = newFiber;}prevSibling = newFiber;index++;}// 最后返回下一个工作单元// 先判断是否是子节点。然后是兄弟节点,最后是父节点if (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;}
} 

其中,原来的render函数改为了createDom函数,用于创建DOM节点,在performUnitOfWork中调用。我们在上一节定义的nextUnitOfWork对象即为fiber数据结构,而在render函数中我们将nextUnitOfWork设为fiber tree的根节点。当浏览器空闲时,会调用workLoop并从根节点开始遍历。我们通过fiber.dom来追踪DOM节点。

两个fiber tree

这里没有实现的是,React会维护两个fiber tree,一个称之为current tree,与当前DOM对应;另一个叫做work in progress tree,与期望更新后的DOM对应。diff出来的修改会直接更新在 work in progress tree中,然后将current指针和workInProgress指针对调,从而实现更新。这种技巧我们称之为 Double Buffering

这一部分将在下一节探讨。

5. Render和Commit阶段

if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom)
} 

当我们向DOM树中添加一个新元素的时候,浏览器可能会在渲染完整个树之前打断渲染工作,因此用户可能会遇到卡顿或者不完整的页面。而以上这部分在performUnitOfWork中的更新DOM节点的方法需要被拿掉,用更好的方法代替。

以下代码加入了上一节提到的work in progress tree,命名为wipRoot,表示追踪fiber tree的根节点。

// work in progress root
let wipRoot = null;

// 将render函数中的nextUnitOfWork换成wipRoot(work in progress tree)
function render(element, container) {wipRoot = {dom: container,props: {children: [element]}};nextUnitOfWork = wipRoot;
}

// 将节点添加到DOM树中
function commitRoot() {commitWork(wipRoot.child);wipRoot = null;
}

// 递归地将节点添加到DOM树中
function commitWork(fiber) {if (!fiber) {return;}const domParent = fiber.parent.dom;domParent.appendChild(fiber.dom);// 递归添加子节点和兄弟节点commitWork(fiber.child);commitWork(fiber.sibling);
}

function workLoop(deadline) {let shouldYield = false;while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);shouldYield = deadline.timeRemaining() < 1;}// 当render阶段的所有工作完成后(因为没有下一个单元的工作了),我们就把整个fiber树commit到DOM中if (!nextUnitOfWork && wipRoot) {commitRoot();}requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber);}const elements = fiber.props.children;let index = 0;let prevSibling = null;while (index < elements.length) {const element = elements[index];const newFiber = {type: element.type,props: element.props,parent: fiber,dom: null,};if (index === 0) {fiber.child = newFiber;} else {prevSibling.sibling = newFiber;}prevSibling = newFiber;index++;}if (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;}
} 

对render&commit阶段优化的处理如下:

1.把performUnitOfWork中关于把子节点添加至父节点的逻辑删除;
2.新增一个根节点变量wipRoot,存储 fiber 根节点;
3.当所有 fiber 都工作完成时,nextUnitOfWorkundefined,这时再渲染真实 DOM;
4.新增commitRoot函数,执行渲染真实 DOM 操作,递归地将 fiber tree 渲染为真实 DOM;

6. Reconciliation协调

上一节中实现了递归地将节点添加到了DOM树中,接下来实现更新或删除节点。我们需要将在render函数中得到的元素和上一次commit到DOM的fiber tree进行一个对比。

因此,我们需要将“上一次commit到DOM的fiber tree”的引用保存下来,称其为current tree,用currentRoot来追踪current tree的跟节点。

同时,我们也添加了alternate属性到每个fiber,它是一个链接到旧fiber的一个属性,旧fiber指的是我们在前一个commit阶段提交更新到DOM的fiber。

function render(element, container) {wipRoot = {dom: container,props: {children: [element]},// 链接到旧fiber的属性,指在前一个commit阶段提交更新到DOM的fiberalternate: currentRoot};// render时,初始化 deletions 数组deletions = [];nextUnitOfWork = wipRoot;
}

let nextUnitOfWork = null;
// work in progress root
let wipRoot = null;
// 用current tree保存“上一次commit到DOM的fiber tree”的引用
let currentRoot = null;
// 新建deletions数组,存储需删除的fiber节点,渲染DOM时,遍历deletions删除旧fiber
let deletions = null;

function commitRoot() {// 渲染DOM时,遍历deletions删除旧fiberdeletions.forEach(commitWork);commitWork(wipRoot.child);// 交换两棵树的指针currentRoot = wipRoot;wipRoot = null;
}

function commitWork(fiber) {if (!fiber) return;const domParent = fiber.parent.dom;if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {// 当fiber的effectTag为PLACEMENT时,表示新增fiber,将该节点新增至父节点中domParent.appendChild(fiber.dom);} else if (fiber.effectTag === "DELETION") {// 当fiber的effectTag为DELETION时,表示删除fiber,将父节点的该节点删除domParent.removeChild(fiber.dom);} else if (fiber.effectTag === 'UPDATE' && fiber.dom !== null) {// 当fiber的effectTag为UPDATE时,表示更新fiber,更新props属性updateDom(fiber.dom, fiber.alternate.props, fiber.props);}// 递归实现commitWork(fiber.child);commitWork(fiber.sibling);
}

function workLoop(deadline) {let shouldYield = false;while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);shouldYield = deadline.timeRemaining() < 1;}if (!nextUnitOfWork && wipRoot) {commitRoot();}requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber);}const elements = fiber.props.children;reconcileChildren(fiber, elements);if (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;}
}

function reconcileChildren(wipFiber, elements) {let index = 0;// oldFiber可以在wipFiber.alternate中找到let oldFiber = wipFiber.alternate && wipFiber.alternate.child;// 上一个兄弟节点let prevSibling = null;while (index < elements.length || oldFiber != null) {const element = elements[index];let newFiber = null;// fiber类型是否相同const sameType =oldFiber &&element &&element.type == oldFiber.type;// 如果类型相同,仅更新propsif (sameType) {newFiber = {type: oldFiber.type,props: element.props,dom: oldFiber.dom,parent: wipFiber,alternate: oldFiber,effectTag: "UPDATE",};}// 新旧fiber类型不同,且有新元素时if (element && !sameType) {// 创建一个新的 dom 节点,设置 effectTag 为 PLACEMENTnewFiber = {type: element.type,props: element.props,dom: null,parent: wipFiber,alternate: null,effectTag: "PLACEMENT",};}// 新旧fiber类型不同,且有旧fiber时if (oldFiber && !sameType) {// 删除旧fiber,设置effectTag为DELETIONoldFiber.effectTag = "DELETION";// 加入待删除节点列表deletions.push(oldFiber); }if (oldFiber) {oldFiber = oldFiber.sibling;}// fiber的第一个子节点if (index === 0) {wipFiber.child = newFiber;} else if (element) {// fiber的其他子节点,它第一个子节点的兄弟节点prevSibling.sibling = newFiber;}// 把新建的newFiber赋值给prevSibling,在下一个循环为newFiber添加兄弟节点prevSibling = newFiber;index++;}
} 

我们将performUnitOfWork中创建新fibers的代码提取出来,封装在一个reconcileChildren函数中,在这里用新元素将旧fiber协调一致成新fiber。

同时,我们也会迭代更新旧fiber的子节点(wipFiber.alternate)。其中,element是我们想到更新到DOM的东西,oldFiber是上一次render的东西。我们需要对它们进行比较,找出需要进行的更新工作(diff)。当 element 有更新时,需要将更新前的 fiber tree 和更新后的 fiber tree 进行比较,得到比较结果后,仅对有变化的 fiber 对应的 DOM 节点进行更新。 通过协调,减少对真实 DOM 的操作次数。

diff/更新/协调(reconciliation)分为三种情况,分别用三种effectTag进行标识:

1.当新旧 fiber 类型相同时:保留 DOM,仅更新 props,设置 effectTag 为 UPDATE
2.当新旧 fiber 类型不同,且有新元素时:创建一个新的 DOM 节点,设置 effectTag 为 PLACEMENT
3.当新旧 fiber 类型不同,且有旧 fiber 时:删除旧 fiber,设置 effectTag 为 DELETION,我们需要设置一个数组来保存需要删掉的节点,即deletions

effectTag属性被添加到fiber中,在之后的commit阶段使用。对应的,在commitRoot函数中添加遍历删除节点的步骤,同时在commitWork函数中添加对应处理不同effectTag的逻辑。其中,为UPDATE的情况封装了一个updateDom函数,实现如下:

const isEvent = key => key.startsWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
// // 是否是新属性
const isNew = (prev, next) => key => prev[key] !== next[key];
// 是否是旧属性
const isGone = (prev, next) => key => !(key in next);

function updateDom(dom, prevProps, nextProps) {// 移除旧的或者已经改变的事件监听器Object.keys(prevProps).filter(isEvent).filter(key =>!(key in nextProps) ||isNew(prevProps, nextProps)(key)).forEach(name => {const eventType = name.toLowerCase().substring(2)dom.removeEventListener(eventType,prevProps[name])});// 移除旧属性Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {dom[name] = ""});// 设置新的或改变后的属性Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {dom[name] = nextProps[name]});// 添加事件监听器Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps)).forEach(name => {const eventType = name.toLowerCase().substring(2)dom.addEventListener(eventType,nextProps[name])});
} 

其中,我们比较了新旧fiber的 props,删除旧props,设置新的或者已经改变的props。

另外,还有一种特殊的属性需要更新,那就是event listeners事件监听器,因此需要特殊处理;如果事件监听器发生了改变,那么就先把它删除,最后需要添加新的事件监听器。详见代码注释。

7. Function Components函数组件

这一节我们添加myReact对函数组件的支持。

函数式组件和类组件都能实现相同的效果。但是他们有一些区别,体现在以下几个方面:

1.设计思想不同:函数式组件是函数式编程思想,而类组件是面向对象编程思想。面向对象编程将属性和方法封装起来,屏蔽很多细节,不利于测试;
2.类组件有状态管理,而函数式组件的状态需要使用useState自定义;
3.创建组件时,函数式组件只需调用函数即可创建组件,而类组件必须先实例化一个对象,然后通过这个实例化对象调用render函数来创建组件;
4.类组件是用生命周期钩子函数来实现业务逻辑的,而函数式组件使用react Hooks来实现业务逻辑;
5.函数组件的 fiber 没有 DOM 节点;
6.函数组件的 children 需要运行函数后得到,而不是直接从 props 中得到;

因此,我们需要检查fiber的类型是否为一个函数,然后使用不同的更新方法。通过下列步骤实现函数组件:

1.修改 performUnitOfWork,根据 fiber 类型,执行 fiber 工作单元;
2.定义 updateHostComponent 函数,执行非函数组件;
3.定义 updateFunctionComponent 函数,执行函数组件;
4.修改 commitWork 函数,兼容没有 DOM 节点的 fiber: a. 修改 domParent 的获取逻辑,通过 while 循环不断向上寻找,直到找到有 DOM 节点的父 fiber;b. 修改删除节点的逻辑,当删除节点时,需要不断向下寻找,直到找到有 DOM 节点的子 fiber;

function performUnitOfWork(fiber) {// 是否是函数类型组件const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function;// 如果是函数组件,执行 updateFunctionComponent 函数if (isFunctionComponent) {updateFunctionComponent(fiber);} else {// 如果不是函数组件,执行 updateHostComponent 函数updateHostComponent(fiber);}if (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;}
}

// 定义 updateFunctionComponent 函数,执行函数组件
// 函数组件需要运行来获得 fiber.children
function updateFunctionComponent(fiber) {// fiber.type 就是函数组件本身,fiber.props 就是函数组件的参数const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}

// 定义 updateHostComponent 函数,执行非函数组件
function updateHostComponent(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber);}reconcileChildren(fiber, fiber.props.children);
} 

updateHostComponent函数中,我们进行和之前一样的操作,创建节点并协调子节点。而在updateFunctionComponent函数中,我们需要运行该函数来获得 fiber.children。

最后对commitWork 函数进行修改,实现如下:

function commitWork(fiber) {if (!fiber) return// 修改 domParent 的获取逻辑,通过 while 循环不断向上寻找,直到找到有 DOM 节点的父 fiberlet domParentFiber = fiber.parent;// 如果 fiber.parent 没有 DOM 节点,则继续找 fiber.parent.parent.dom,直到有 DOM 节点while (!domParentFiber.dom) {domParentFiber = domParentFiber.parent;}const domParent = domParentFiber.dom;if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {// 当fiber的effectTag为PLACEMENT时,表示新增fiber,将该节点新增至父节点中domParent.appendChild(fiber.dom);} else if (fiber.effectTag === "DELETION") {// 如果 fiber 的更新类型是删除,执行 commitDeletioncommitDeletion(fiber.dom, domParent);} else if (fiber.effectTag === 'UPDATE' && fiber.dom !== null) {// 当 fiber 的 effectTag 为 UPDATE 时,表示是更新 fiber,更新 props 属性updateDom(fiber.dom, fiber.alternate.props, fiber.props);}commitWork(fiber.child);commitWork(fiber.sibling);
}

// 修改删除节点的逻辑,当删除节点时,需要不断向下寻找,直到找到有 DOM 节点的子 fiber
function commitDeletion(fiber, domParent) {if (fiber.dom) {// 如果该 fiber 有 DOM 节点,直接删除domParent.removeChild(fiber.dom);} else {// 如果该 fiber 没有 DOM 节点,则继续找它的子节点进行删除,递归实现commitDeletion(fiber.child, domParent);}
} 

8. Hooks钩子

React还有一个重要的特性就是Hooks(钩子)。当有了函数组件之后就需要添加state。

考虑以下经典的计数器例子,我们添加useState钩子来获取并更新counter的值。

const myReact = {createElement,render,useState
};

const container = document.getElementById("container");

function Counter() {const [count1, setCount1] = myReact.useState(1);const [count2, setCount2] = myReact.useState(2);return (<div><h1>Count1: {count1}</h1><button onClick={() => setCount1(count1 => count1 + 1)}>add1</button><h1>Count2: {count2}</h1><button onClick={() => setCount2(count2 => count2 + 2)}>add2</button></div>)
};
const element = <Counter />;

myReact.render(element, container); 

对于useState的实现,我们需要进行以下步骤:

1.新增全局变量 wipFiberhookIndex,保存当前 fiber(工作单元/函数组件) 和 当前 fiber 的 hooks 下标;2.新增 useState 函数:a. 判断是否有旧钩子,旧钩子存储了上一次更新的 hook;b. 初始化钩子,钩子的状态是旧钩子的状态或者初始状态;c. 从旧的钩子队列中获取所有动作,然后将它们应用到新的钩子状态;d. 设置钩子状态/setState: 将动作添加到钩子的队列中并更新渲染e. 钩子添加至工作单元的钩子列表f. 返回钩子的状态和设置钩子的函数

// 新增全局变量 wipFiber, 保存当前工作单元 fiber
let wipFiber = null;
// 新增全局变量 hookIndex,保存当前工作单元 fiber 中的hooks数量
let hookIndex = null;

// 修改updateFunctionComponent函数,在函数组件更新的时候重置两个变量
function updateFunctionComponent(fiber) {wipFiber = fiber;// 当前工作单元 fiber 的 hookhookIndex = 0;wipFiber.hooks = [];const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}

function updateRender() {wipRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot};// 设置下一个工作单元nextUnitOfWork = wipRoot;// 清空需删除的节点deletions = [];
}

// 新增 useState 函数
function useState(initial) {// 是否有旧钩子,旧钩子存储了上一次更新的 hookconst oldHook =wipFiber.alternate &&wipFiber.alternate.hooks &&wipFiber.alternate.hooks[hookIndex];// 初始化钩子,钩子的状态是旧钩子的状态或者初始状态const hook = {state: oldHook ? oldHook.state : initial,queue: []};// 从旧的钩子队列中获取所有动作,然后将它们应用到新的钩子状态const actions = oldHook ? oldHook.queue : [];actions.forEach(action => {hook.state = action(hook.state)});// 设置钩子状态的setState函数let setState = action => {// 将动作添加至钩子队列hook.queue.push(action);// 更新渲染,新建 updateRender 函数updateRender();};// 把钩子添加至工作单元// wipFiber.hook = hook;wipFiber.hooks.push(hook);hookIndex++;// 返回钩子的状态和设置钩子的函数return [hook.state, setState];
} 

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值