目录
一、Fiber
二、执行过程
挂载
更新
卸载
三、Dom diff
策略
源码分析
reconcileChildFibers
reconcileSingleElement
reconcileSingleTextNode
reconcileChildrenArray
工具函数
小结
lastPlacedIndex
这里我们以
React 17.0.3版本为例。
一、Fiber
React 从 16 开始引入了 Fiber。
在 Fiber 架构前,当 React 决定要加载或者更新组件树时,会做一个“大动作”。这个动作包括生命周期的调用、diff 过程的计算、DOM 树的更新等等。这个动作很大,耗时不短,并且它还是同步进行的,一旦开始就不能中断。这意味着你在 挂载/更新 结束前,啥也不能干。
面对“单个任务耗时过长”这个问题,解决思路是把一个庞大的任务拆分成 N 多个微小的任务(如下图)
每个微小的任务就叫 Fiber,它代表着一个单位的工作,也是接受调度的最小单元。
上图中每一个波峰和波峰之间,就意味着是一个工作单元(Fiber)。每次到达波峰时,意味着该任务交出了对主线程的占用。
每完成一个小任务,都会暂停一下对主线程的占用,看看有没有优先级更高的事情需要处理。以此来确保主线程总在做它当下最应该做的事情。
这种新的调和方式,叫做 Fiber Reconciler。
二、执行过程
(图片截取自 React 官网,版本>= 16.4 点这里查看)
分为三个阶段:挂载、更新、卸载
挂载
组件实例被创建并插入 DOM 中时
生命周期函数调用顺序如下:
constructor():初始化 state 或进行方法绑定static getDerivedStateFromProps():每次渲染前都会触发,不常用render():检查 this.props 和 this.state 的变化并根据返回值的不同做出不一样的渲染策略componentDidMount():组件挂载后(插入 DOM 树中)立即调用
更新
组件的 props 或 state 发生变化时触发更新
生命周期函数调用顺序如下:
static getDerivedStateFromProps():同上shouldComponentUpdate():根据返回值判断 state 或 props 的变化是否重新渲染,多用于性能优化,不常用render():同上getSnapshotBeforeUpdate():最近一次渲染输出(提交到 DOM 节点)之前调用,即图中 Pre-Commit 阶段,不常用componentDidUpdate():完成更新后被立即调用
卸载
组件从 DOM 中移除时会调用如下方法:
componentWillUnmount():组件卸载及销毁之前直接调用
三、Dom diff
策略
时间复杂度为 O(n^3) 的传统 diff 算法显然无法满足框架对性能的要求,因此,React 团队根据前端界面的特性,提出三条假设:
-
相同的组件有着相同的 DOM 结构,不同的组件有着不同的 DOM 结构(
component diff) -
位于同一层次的一组子节点,它们之间可以通过唯一的 id 进行区分(
element diff) -
DOM 结构中,跨层级的节点操作非常少,可以忽略不计(
tree diff)
基于这三条假设将 diff 算法的时间复杂度从 O(n^3) 降到了 O(n)
更新逻辑
在 React 中,Fiber 相当于虚拟节点
这里截取了部分属性
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key; // key 值
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null; // 子节点
this.sibling = null; // 兄弟节点
this.index = 0; // 节点下标
...
this.alternate = null;
...
}
我们来看一下具体是怎么更新视图的
从根节点开始:
div节点通过 child 属性找到节点div1div1节点通过 sibing 属性找到节点ulul节点通过 child 属性找到节点lili节点与自身的 alternate 属性存放的节点信息比较,比较完成后把更新 commit3 通过 return 提交到ul节点ul节点与自身的 alternate 属性存放的节点信息比较,比较完成后生成 commit2,连同commit3一同 return 给div节点div1节点与自身的 alternate 属性存放的节点信息比较,比较完成后把更新 commit1 通过 return 提交到div节点- 获取到所有更新(commit1-3)后,再一次更新到真实 dom 中
源码分析
这里就不一步一步慢慢查找代码了,这里直接找到 react\packages\react-reconciler\src\ReactChildFiber.new.js 文件。
reconcileChildFibers
该方法用于判断节点类型,针对不同类型使用不同的调和函数处理节点
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any, // New fiber
lanes: Lanes,
): Fiber | null {
// 若 newChild 是未设置 key 值的 Fragment 类型节点(顶层节点)
// 则将其子节点赋值给 newChild
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
// newChild 是个对象
switch (newChild.$$typeof) {
// 单节点类型
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// 后面 case 的处理方式与单节点类型类似,就不一一分析了
...
}
if (isArray(newChild)) {
// 数组类型
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
...
// 无效的对象类型
throwOnInvalidObjectType(returnFiber, newChild);
}
// 文本节点
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
...
// 更新删除掉了所有节点,执行删除
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
reconcileSingleElement
处理 New fiber 为单个节点的情况
function reconcileSingleElement(
returnFiber: Fiber, // 即 workInProgress
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild; // Old fiber
// 若 child 不为 null,则一直循环
while (child !== null) {
if (child.key === key) {
// key 值相同
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// 类型一致,可复用 Old fiber,删除同级兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
...
return existing;
}
} else {
if (
child.elementType === elementType ||
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
(enableLazyElements &&
typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// 类型一致,可复用 Old fiber,删除同级兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
...
return existing;
}
}
// 类型不一致,无法复用,删除当前 child
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key 值不一致,无法复用,删除当前 child
deleteChild(returnFiber, child);
}
// 走到这说明当前 child 不可被复用
// 改用 child 的同级兄弟节点来继续比较(优化点)
child = child.sibling;
}
// 上面如果执行完没有可复用的 则进入这里进行创建
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
reconcileSingleTextNode
接下来处理文本节点类型的 New fiber
function reconcileSingleTextNode(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
lanes: Lanes
): Fiber {
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
// 类型一致,则直接复用,删除同级兄弟节点
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent);
existing.return = returnFiber;
return existing;
}
// 类型不一致,无法复用,删除节点,创建新节点
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
reconcileChildrenArray
最后是处理数组类型的 New fiber,大多数文章讨论的最多的就是这部分
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
...
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 第一次遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
// 旧节点位于新节点右边
// step 1: 下次循环,旧节点不变,向右取新节点
nextOldFiber = oldFiber;
oldFiber = null;
} else {
// step 2: 使用旧节点的兄弟节点作为下一次遍历的旧节点(循环末尾将 nextOldFiber 赋值给 oldFiber)
nextOldFiber = oldFiber.sibling;
}
// step 3: 根据 key 判断是否可以复用节点
// 若可复用则返回旧节点创建的 fiber
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
// step 4: 不可复用节点,跳出循环
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// step 5:删除旧节点
deleteChild(returnFiber, oldFiber);
}
}
// step 6:更新操作
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
...
// step 7:将后一个节点赋值给 oldFiber
oldFiber = nextOldFiber;
}
// 第一次遍历结束
if (newIdx === newChildren.length) {
// step 8:新节点都遍历完了,删除剩下的旧节点
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// 旧节点遍历完了
// step 9:新节点若还有,则进入下面的循环逐一创建
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// step 10:使用 Map 保存旧节点信息
// 若旧节点存在 key 值,则使用 key 值作为 Map 的键值
// 若不存在 key 值,则使用下标作为 Map 的键值
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 第一次循环过程中碰到无法复用的节点便会跳出,走第二次循环
// 第二次遍历
for (; newIdx < newChildren.length; newIdx++) {
// step 11:通过新节点的 key 值或索引,查找 existingChildren 是否有相同的旧节点
// 若不存在,则会为新节点创建新 Fiber
// 下一小节详解
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// step 12:若 newFiber 是复用的旧节点,则删除 existingChildren 中对应的节点
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// step 13:更新操作
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
...
}
}
// step 14:完成第二次遍历后,清空 existingChildren
if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
工具函数
流程基本走完了,看一下几个重要的工具函数
// 在 reconcileChildFibers 方法中处理文本节点的插入操作
// shouldTrackSideEffects 标识, 是否为 Fiber 对象添加 effectTag
// shouldTrackSideEffects 为 true 代表更新操作
// alternate 属性为 null,表示该 fiber 还未插入到 Dom 中
function placeSingleChild(newFiber: Fiber): Fiber {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement;
}
return newFiber;
}
// 在 reconcileChildrenArray 方法的第二次遍历中使用
// 遍历当前同级旧节点,得到以 key 值 或 索引 为 key,节点为值的 Map
function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber
): Map<string | number, Fiber> {
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
// 在 reconcileChildrenArray 方法的第二次遍历中使用
// 通过新节点的 key 值或索引,查找 existingChildren 是否有相同的旧节点
function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any,
lanes: Lanes
): Fiber | null {
if (typeof newChild === "string" || typeof newChild === "number") {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(returnFiber, matchedFiber, "" + newChild, lanes);
}
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const matchedFiber =
existingChildren.get(newChild.key === null ? newIdx : newChild.key) ||
null;
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_PORTAL_TYPE: {
...
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
}
...
}
...
return null;
}
// 在 reconcileChildrenArray 方法中两次循环均有使用
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// 不需要更新
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// 可复用旧节点在新节点左侧,则使用移动操作,即 可复用旧节点右移
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// 不动它
return oldIndex;
}
} else {
// 新节点的插入操作
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
小结
-
若 New fiber 为文本类型,判断与 Old fiber 类型是否为文本(文本内容不需要一样)
- 是,则删除 Old fiber 的同级兄弟节点,并复用 Old fiber
- 不是,则删除 Old fiber,并创建新的文本节点
-
若 New fiber 为单节点,判断 key 值是否相等
- 相等,则继续判断节点类型是否一致
- 一致,则删除 Old fiber 的同级兄弟节点,并复用 Old fiber
- 不一致,则删除当前 Old fiber,使用当前 Old fiber 的兄弟节点重新判断 key 值、类型
- 不相等,则删除当前 Old fiber,使用当前 Old fiber 的兄弟节点重新判断 key 值、类型
- 相等,则继续判断节点类型是否一致
-
若 New fiber 为数组类型(存在多个节点)
- 第一次遍历,比较相同位置的新旧节点,判断 key 值是否一致
- 一致,则表示可复用,复用当前旧节点
- 不一致,则跳出循环
- 第一次遍历结束
- 若新节点已遍历完了、旧节点还有剩余,则删除旧节点,返回结果,结束调和过程
- 若旧节点遍历完了、新节点还有剩余,则遍历新节点创建新 Fiber,并插入,返回结果,结束调和过程
- 若新旧节点都有剩余,即第一次遍历时跳出循环了,则遍历 Old fiber 得到以 Key 值 或 索引 作为 键值,旧节点作为值的 Map,开始第二次遍历
- 第二次遍历,通过上一步得到的 Map,判断 Old fiber 中是否有相同 key 值的节点
- 存在,取出旧节点,复用旧节点,插入操作,之后删除 Map 中对应的旧节点
- 不存在,则新建(在
updateFromMap中实现)
- 第一次遍历,比较相同位置的新旧节点,判断 key 值是否一致
lastPlacedIndex
最后来看看贯穿 diff 算法的 lastPlacedIndex 到底是干嘛用的。
lastPlacedIndex 记录着上一个节点的位置。在更新操作时,比较需要更新的节点的下标与 lastPlacedIndex,当下标小于 lastPlacedIndex 时,才需要移动,即:将节点右移。
文章同时发在个人公众号,欢迎关注 MelonField