开篇前,先聊下几个概念:
1. jsx 是什么?
jsx 其实就是js的语法拓展,写代码时用js描述UI更加方便;最终它会经过babel等转码器编译成可运行的js; 例如
const redDivDesc = <div style={{color: isRed ? 'red' : ''}}> red div</div>
会转码为如下代码:
const redDivDesc = _jsxRuntime.jsx("div", {
style: {
color: isRed ? 'red' : ''
},
children: " red div"
});
通过以上也可以看出来,在js语法上扩展jsx来描述UI 是很灵活的;
2. react element是什么?
上例中jsx函数的返回结果,例如redDivDesc就是一个react element; 它有以下几个属性:
$$typeof: REACT_ELEMENT_TYPE, // 标识是否是ReactElement
type: 'div', // 'div' () => xxx 等,jsx 的第一个入参;
key: key, // 设置的key
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner
3. fiber是什么?
fiber 是vDom在react中的实现,也是react新架构的基础;
它既承担了存储数据的职责,也作为react的工作单元,保存了更新变化的数据和接下来要执行的工作;一个fiber的结构如下:
// Instance
this.tag = tag; // HostComponent:5 HostText:6 FunctinComponent: 0 ClassComponent: 1 ...
this.key = key;
this.elementType = null;
this.type = null; // dom元素指向对应的元素类型,组件只想其函数或者类
this.stateNode = null; // 指向真实DOM元素,类组件指向其实例
// Fiber
this.return = null; // 指向父级节点
this.child = null;
this.sibling = null; // 兄弟节点 (注意子节点是一个链表,不是数组)
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode; //描述fiber树的模式,比如 ConcurrentMode 模式
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null; // 要删除的子节点
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null; // 指向上一次可复用的fiber
通过 react Element的信息是可以创建fiber的,这是我们写的jsx到生成fiber的基础;例如可以这样
class FiberNode {
constructor (tag, props, key) {
// ... 一堆属性的初始化
this.tag = tag;
this.props= props;
this.key = key;
}
}
export function createFiberFromElement(
element: ReactElement
): Fiber {
const {type, key, props} = element;
const tag = getTagFromType(type);
reurn new FiberNode(tag, props, key);
}
从jsx代码到fiber树的生成
考虑如下代码
const list1 = [
{ text: "a", color: "red" },
{ text: "b", color: "green" },
{ text: "c", color: "yellow" },
{ text: "d", color: "blue" },
];
const list2 = [
{ text: "d", color: "blue" },
{ text: "a", color: "red" },
{ text: "b", color: "green" },
{ text: "c", color: "yellow" },
];
function List() {
const [list, setList] = useState(list1);
return (
<ul onClick={() => setList(list2)}>
{list.map((item, index) => (
<li key={item.text} style={{ color: item.color }}>
{item.text}
</li>
))}
</ul>
);
}
function App() {
return (
<div>
<List />
oh,hei
</div>
);
}
const root = document.getElementById('root');
createRoot(root).render(<App/>);
代码执行的入口是从createRoot(root).render()开始的; 首先createRoot 执行完会创建 fiberRootNode, 和hostRootFiber, 如下图:
到执行render的时候,会从hostRootFiber克隆一个fiber,生成如下结构,同时产生一个更新(这个更新最终就是把<App/> 存储到hostRootFiber里,注意这时准确的是记录要干啥,实际做的时候是在beginwork中,总之就是后面能拿到 就是了),开始调度和生成fiber树;此时会将一个指针workInprogressFiber指向它
下面就开始了beginWork流程:
总的就是通过当前节点的current.child(fiber 也就是当前节点alternate指向的fiber的子fiber) 与 React Element(下面的child element)比对,然后生成当前节点的child fiber;
首先来对比HostRoot的 current.child 与 child element;这里需要注意的是,每个不同tag获取child element 的方式是不同的,比如HostRoot 经过更新后可以从它的属性中获取它的child element,即App;
此时通过比对, 发现current.child是null, 那么会通过element来生成fiber(current.child 不是null,有可能会走复用,后面再说);
// 转码后的ReactElement,createFiberFromElement前面已经提到了;
newFiber = createFiberFromElement({
type: App,
key: null,
ref: null,
props: {}
});
因为我们现在知道workInprogress是它的父fiber,所以可以建立如下关系
newFiber.return = workInprogress;
workInProgress.child = newFiber;
workInProgress = newFiber;
现在开始比对App fiber 的current.child 与 它的child element; 针对function Component的fiber,它的child是它运行的结果即App()的返回值;同样他没有current.child,后面的流程也是如此,不再特意说明,所以也是通过createFiberFromElement来创建fiber;执行完后如图(后面通过wip fiber来指定workInproressFiber)
进入div的current.child 和 child element; 对于HostCompoennt(原始dom节点的fiber)它的child element 在它的属性props.children中,jsx 编译后会放里面;这个地方比上一步相对复杂一些,要建立起子节点的链表;大概可以这样实现
let firstNewFiber = null,lastNewFiber = null;
for (let i = 0; i< childElements.length; i++) {
const newFiber = createFiberFromElement(childElement[i]);
newFiber.index = i;
newFiber.return = workInprogressFiber;
if (firstNewFiber === null) {
firstNewFiber = newFiber;
lastNewFiber = newFiber;
} else {
lastNewFiber.sibling = newFiber;
lastNewFiber = lastNewFiber.sibling;
}
}
workInprogressFiber = firstNewChild;
现在的fiber树如图:
- 接着对比List组件的current.child和它的child element;
- 接着对比Ul的current.child 和它的child.element;
- 接着对比第一个li的current.child 和它的child.element;
最终fiber树如图:
可以发现,现在到了text 类型的fiber节点,这其实也就是fiber树的叶子节点(文本不可能有子节点了);但是还有几个li的子节点没有生成,那么后续我们要从当前寻找sibling,对它的slibling执行上述过程;然后逐级往上寻找sibling;用代码概括一下这个流程:我们将它标记为流程1;
// workInProgressFiber => wipFiber
let wipFiber = hostRootFiber;
do {
down: while (wipFiber) {
let childFiber = beginWork(wipFiber);
if (childFiber === null) {
break down;
}
wipFiber = childFiber;
}
up: while (wipFiber !== null) {
completeWork(wipFiber);
if (wipFiber.sibling === null) {
wipFiber = wipFiber.return;
} else {
wipFiber.sibling.return = wipFiber.return;
wipFiber = wipFiber.sibling;
// 回归到beginwork流程;
break up;
}
}
if (wipFiber === null) {
break;
}
} while (true)
在beginWork 中,如果遇到text, 返回null; 开始从叶子节点往上;直到HostRootFiber(return 为null);这个过程会遍历到树中的每个节点,并调用completeWork;我们还是先生成代码声明的整颗fiber树后再说它; 根据上面流程,我们后面会进行如下步骤:
- 没有sibling,向上
- 有sibling,开始 slibling 的 beginwork;
生成fiber树如图
现在遇到了a 节点的情况,继续同样的流程,我们就得到了完整的fiber树
我们在绿色步骤处可以做一下DOM生成;然后放在fiber中存储; 在 叶子 fiber中我们可以创建Dom 节点,存储在stateNode中, 然后在任意HostComponent节点往下寻找有stateNode的节点,然后appendChild;依次执行,就会得到一颗离屏DOM;(序号代表Dom 生成的顺序);
最终会查找标记了Placement的节点的第一个有stateNode的节点,append到父节点上;图中就是将离屏div 挂载到root div上;最终浏览器进行渲染即得到了我们代码要得到的结果;
最终执行完后,FiberRootNode 的current指针指向当前fiber树的HostRootFiber;
更新后fiber树的变化
当我们点击了ul触发了setState,又会触发一个更新,react 开始新一轮的流程1;
总的来说,这个流程也是生成fiber树,但是跟首屏渲染的不同是,我们要找出变化的节点,只去对变化的节点进行操作,提高性能;这里面比对时大部分节点都有current.child,根据某种规则,我们会复用current fiber tree 中的fiber,这个规则实现就是diff算法;下面每个步骤都会涉及;
diff分为单节点diff和多节点diff(单节点和多节点diff指的是child element)
单节点diff:
涉及到的情况:
- li(key=1) li(key=2) li(key=3) -> li(key=3); 删除多个节点;
- li(key=1) li(key=2) li(key=3) -> li(key=4); 删除;新增
- li(key=1) -> p(key=1); Component(key=1) -> li(key=1); key相同,type不同; 删除新增
- null -> div; 新增
逻辑如下:
function reconcileSingleElement(wipFiber, childFiber, childElement) {
let sameLevelFiber = childFiber;
// 挨个寻找上次更新的同级节点是否有相同的key的节点
while (sameLevelFiber !== null) {
if (sameLevelFiber.key === childElement.key) { // key相同
if (childElement.$$typeof === REACT_ELEMENT_TYPE) {
if (childElement.type === sameLevelFiber.type) { // type 相同 div -> div p->p Component->Component
const fiber = reUseFiber(sameLeveFiber);
fiber.return = wipFiber;
// 因为key是唯一的,所以剩下的肯定是要删掉的;
deleteRemainingChildren(wipFiber, sameLevelFiber.sibling);
return fiber;
} else {
// 既然相同的key,type不同,那么以前的节点都要删除;将sameLevelFiber及其后面的兄弟节点放到wipFiber的deletions中
deleteRemainingChildren(wipFiber, sameLevelFiber);
break;
}
} else {
break;
}
} else {
deleteChild(wipFiber, sameLevelFiber);
// 这一个key 不同,换下一个
sameLevelFiber = sameLevelFiber.sibling;
}
}
// 没有找到相同的key节点;新建一个;
const fiber = createFiberFromElement(childElement);
fiber.return = wipFiber;
return fiber;
}
此处需要注意一下,reUseFiber就是从一个fiber的信息来创建另一个fiber, 并且相互之间建立一个alternate的联系;
从element新建的fiber里面很多信息都是初始值,例如alternate指向的是null;
多节点diff
- 预处理同级fiber节点为map(key->fiber)结构;
- 遍历child element,根据上一步生成的map信息,通过重用(map中存在key相同的且type相同,此时删除map中的key)或者从element创建来生成新的fiber;同时填充index,sibling数据;打flag标记(标记是否移动);
- 将map中剩余节点标记删除
可以看出,不论是多节点还是单节点,都是同级的对比,这样遍历一遍每个节点比较一次,是O(n)的复杂度;而不是两个树的对比,跨层级的移动是不会重用节点的,所以开发中我们也应该减少跨层级的移动;
后面通过demo来看这个过程;
初始状态
wip Fiber 是HostRootFiber,现在生成它的child fiber;它的child element 是 App element, current.child 是App fiber, 这是一个单节点diff, 它们的key相同,type也相同(都是App function的引用), 所以会根据App fiber来生成 child fiber,生成后如下图:
- 对比App的current.child fiber与App child element;
- 对比Div的current.child fiber与Div child element;
- 对比Ul 的current.child fiber与Ul 的child element;
下面依次进行以上步骤,以下对比的子fiber与子element key 相同,type也相同;fiber树结构如下:
生成ul的child fiber是一个多节点diff的例子:
- 先用current.child 生成 map;
let node = current.child;
const map = new Map();
while (node !== null) {
const usedKey = node.key === null ? node.index : node.key;
map.set(usedKey,node);
node = node.sibling;
}
- 循环child element, 逻辑如下:
let lastFixedIndex = 0;
for (let i = 0; i < childElements.length; i++) {
const el = childElements[i];
const fiber = getFiberFromMap(wipFiber, el, index, map);
if (fiber === null) {
continue;
}
fiber.index = i;
fiber.return = wipFiber;
// ...生成链表代码省略
if (!shouldTrackEffects) { // 不需要跟踪副作用,继续,此种情况是wip fiber没有current的情形;
continue;
}
const current = fiber.alternate;
if (current !== null) {
const oldIndex = current.index;
// 正在遍历到的节点应该比以前遍历的节点的位置要靠右,否则就是向右移动了;
if (oldIndex < maxPlacedIndex) {
newFiber.flags |= Placement;
continue;
} else {
// 不移动
lastFixedIndex = oldIndex
}
} else {
fiber.flags |= Placement;
}
}
function getFiberFromMap (parent, el, index, map) {
const usedKey = node.key === null ? index: node.key;
const match = map.get(usedKey);
if (match) {
// 文本节点需要判断tag,此处省略;
if (el.type === element.type) {
map.delete(usedKey);
return reUseFiber(match, el.props);
}
} else {
return createFiberFromElement(el);
}
}
- 删除map中剩余的fiber;
map.forEach(fiber => deleteChild(parent, fiber);
demo中就是a,b,c 这三个li右移,d 原地不变;
- 返回链表头节点,并把wipFiber指向它
生成的fiber树如图
接着对比li(d)的current child fiber和child element, 文本节点只要fiber.tag 是HostText,就可以重用,demo中的情况是可以重用的;
根据流程1,到文本节点后向上查找sibling,进行fiber生成,最终fiber树结构: