V15
v15中, 主要工作分成两步,调合和渲染,调合就是对jsx对象的对比,负责找出变化。 渲染负责将变化更新到页面中
Stack Reconciler
v15 版本中的栈调户,由reconclier
托管,该模块是整个应用的入口,负责调用不同类型的提供的接口.
在15首次挂载时执行的mount相关的函数,会统一调用被调度对象的mount方法,不同类型的组件开始不同被实现的挂载工作。
在更新中,在batchUpdates
中会对一批需要更新的组件调用reconclier
模块的update
方法,该方法继续调用不同类型不同实现的update方法。
如果是父子关系的组件,会重新创建属于不同类型的对象,在首次挂载时执行reconclier
模块中的mount
方法,或者是更新时的update
方法,依次类推,是一个巨大的函数魂环调用链。
V17
从之前的栈调和变成了可中断的fiber链表结构,在树中,对每个jsx对象都生成对应的fiberNode
Fiber Reconclier
这个阶段在不同的模式下有些区别,现在默认为不可中断的工作,并发模式下会根据shouldYield
判断是否继续工作,由调度器来实现,调合分为两个步骤,递与归,在递阶段首次挂载时会创建整个应用的fiber链表树,在更新时会对比current
和workInprogress
指针指向的两棵树,在更新时就包括diff。 在归阶段为fiberNode打上对应的tag,在commit阶段会被执行本次需要的操作。
beginWork
根据current
指针是否为空来判断时首次挂载还是更新
在首次挂载时会从根节点开始,向下进行深度优先遍历,为遍历到的每一个jsx对象创建对象的fiber链表
在每次遍历到叶子节点child
属性为空时会执行completeWork
, 对新建的fiber节点处理
在满足didReceiveUpdate = false
时, 表示会复用该fiber节点,也就是cloneFiber
begiinWork的函数的目的就是创建fiber链表,并返回下一个工作的子jsx对象。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null; // 父节点
this.child = null; // 子fiber节点
this.sibling = null; // 兄弟节点
this.index = 0; // 在数组类型中的索引位置
this.ref = null;
// 在工作时执行的属性
this.pendingProps = pendingProps; // 组件的最新的props
this.memoizedProps = null; // 组件的props
this.updateQueue = null; // func组件存在的hook变更后的值,原生组件时更新后的数组
this.memoizedState = null; // state
this.dependencies = null; //组件依赖项
this.mode = mode;
// Effect list
this.flags = NoFlags;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
//react 优先级
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 在后续更新时 `current` -> `workInProgress` 两个树的交换,由该属性连接
this.alternate = null;
}
completeWork
该阶段的主要工作就是创建dom节点, 设置属性, 并在首次挂载每次执行的时候把已经存在的子dom节点加入到dom节点下。创建的dom实例保存在stateNode属性上
在completeUnitOfWork
函数中存在变更操作时,打上effectTag
, 整个应用中存在这样的fiber节点会构建成具有变更节点组成的effect树, 由fistEffect、lastEffect、nextEffect
相连
在每次工作完成时,会返回下一个需要工作的fiber节点,依次为sibling -> return -> null
在更新时对与非原生的fiber节点不存在这以操纵, 对原生的节点的变更,会把存在变更的props放入一个数组,数组第i项为key, i+1 项为value。 存放在updateQueue
属性上,并打上Update
的标记,此次变更在commit阶段被执行
Diffing
diff为react运行时找到jsx元素变化的一种方式,分成单一节点diff(前后fiber节点的比较)和多节点diff
singleElement
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 一直循环的对比子fiber节点
while (child !== null) {
// key相同进一步diff, key不同为fiber节点打删除的标记 Deletion
if (child.key === key) {
switch (child.tag) {
case Fragment: {
if (element.type === REACT_FRAGMENT_TYPE) {
// 如果是空节点就复用这个fiber节点, 同时删除兄弟节点
// 因为此处是单一节点的diff,前后fiber只能存在一个,所以要删除其他的兄弟节点
return existing;
}
break;
}
case Block:
// 一样
default: {
// 前后type相同,删除兄弟节点,复用当前fiber节点
if (
child.elementType === element.type ||
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
break;
}
}
// 全部删除
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key不同,全部删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 重新创建fiebr节点,并返回
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;
}
}
- key不同的情况下会删除当前fiber节点,并重新创建一个新的fiber node
- key相同 ,type不同,删除并新建fiber节点,同时删除所有的兄弟节点
- key相同,type相同,复用fiber节点删除兄弟节点
多节点的diff
// 第一轮用的函数
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === 'string' || typeof newChild === 'number') {
if (key !== null) {
return null;
}
return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
oldFiber,
newChild.props.children,
lanes,
key,
);
}
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_LAZY_TYPE: {
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return updateSlot(returnFiber, oldFiber, init(payload), lanes);
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
return null;
}
array
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) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
// 没有可复用的节点,终止循环
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 没有匹配的,把旧节点删除
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// 删除节点的情况
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// oldFIber处理完, 剩下的都是新插入的,处理插入的情况
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;
}
// 设置成map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 移动节点的情况
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 删除剩余无法复用的节点
if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
第一轮遍历
- 从0开始,遍历newChildren,和oldFiber比较,判断是否可复用,可复用继续遍历
- key不同 updateSlot返回null,表示不可复用,第一轮遍历结束
- key相同,type不同删除旧的节点,并新建fiber返回
oldFiber === null
或者newIndex < newChildren.length
跳出第一轮遍历
第二轮遍历
- 如果前后两个fiber数组同时遍历完成,此时diff结束、
- newCHilren没有遍历完成,oldFiber完了,表示新增的打上
Placement
标记 - newChildren遍历完成,oldFiber没有,表示删除打上
Deletion
标记 - 二者都没遍历完成,表示移动终止本次循环
- 第三轮遍历
- 把剩余的oldFIber变成一个Map
- 从Map中通过新节点的key,获取旧的fiber节点。 复用旧节点
c. 如果旧的节点有复用,从map中删除旧的节点const matchedFiber = // 所以说react里面如果用index做key,在结构不稳定的情况下就出问题,前后fiber节点颠倒 // 通过key获取fiber existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; if (newChild.type === REACT_FRAGMENT_TYPE) { return updateFragment( returnFiber, matchedFiber, newChild.props.children, lanes, newChild.key, ); }
d.placeChild
处理节点移动的情况,就是index
e. 把map剩余的fiber全部删除,并返回最新的fiber树的头节点Commit
在协调完成以后会传入root
为整个根节点执行变更操作
- 把剩余的oldFIber变成一个Map
commit 阶段可以分为 执行dom操作之前, 执行dom操作, 执行dom之后
重置一些全局变量
处理effect链表
// 把effect链表赋值为firstEffect,每个fiber的effect只会包含他的后代节点
// 把根节点的effect挂在在effectList的尾部,保证所有的effect都会存在effectList中
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点没有effectTag
firstEffect = finishedWork.firstEffect;
}
before mutation
保存优先级,以同步的优先级,执行完毕后再恢复到之前的优先级
处理focus相关commitBeforeMutationEffects
来处理effectList
commitBeforeMutationEffects
- 处理DOM节点渲染/删除后的焦点失去和获取
- 调用
getSnapshotBeforeUpdate
生命周期函数 -
mutation
遍历
effectList
, 执行函数。 在这里执行的是commitMutationEffects
commitMutationEffects
重置文本节点
- 更新ref对象
- 根据
effectTag
处理插入、更新、删除、插入并更新等dom操作 - 更新时调用
useLayoutEffect
的销毁函数layout
调用commitLayoutEffects
函数,遍历执行effectList
调用变更之后的同步回调函数,比如setState的第二个参数,useLayoutEffect
、didMount
赋值ref对象
- 对于类组件,根据
current === null
来区分是挂载还是更新调用didMount、didUpdate
. 对于setState的第二个参数,也是在此时调用 - 函数组件,调用
layoutEffect
的回调函数
双缓存
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
在页面中呈现的是current树,存在根节点的current属性上, 在内部被使用的为workInProgress树,两个树通过alternate指针对每个节点相连,在更新完成以后workInProgress树替换掉当前的current树
离屏渲染在后续工作中同一时间的更新的可能会分成多次还进行更新。在全部更新完成以后统一修改到页面中
比如在某些情况下如果没有采用这种方式,页面会出来白屏或者有卡顿的感觉