一、前言
最近的几次面试都被问到了框架原理。记忆力不太行的我往往背诵完后很快就会忘记,因此打算花21个小时(分3天,每天7小时)去好好的阅读下React框架,了解其中几个关键功能点的执行逻辑。
框架版本:React 17,enableNewReconciler=false
学习方式:通过debug一步步调试JS代码的方式,来深入了解React框架的主流程
文中的代码片段,只保留作者认为重要的代码
二、DAY 2 目标
- mount阶段的commit流程
- setState过程
- setState后renderRootSync和commitRoot的不同
三、mount阶段的commit流程图
只展示主流程,以及伪代码
四、setState过程
react17中:只有在setTimeout、DOM原生事件绑定等React不能管控的方法之内调用setState是同步的,其他情况setState都是异步的。
setState流程
调用组件的enqueueSetState方法
enqueueSetState = function(inst, payload, callback) {
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);
const update = createUpdate(eventTime, lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 更新fiber的updateQueue(把newState赋值给相关字段)
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, lane, eventTime);
if (enableSchedulingProfiler) {
markStateUpdateScheduled(fiber, lane);
}
}
调用enqueueSetState方法
把newState放到class组件对应Fiber的updateQueue.shared.pending字段里,以便在调和阶段里取用
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
调用scheduleUpdateOnFiber
通过ensureRootIsScheduled方法更新syncQueue队列,
并通过executionContext判断是否立即进入调和阶段重新render。
function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
const priorityLevel = getCurrentPriorityLevel();
if (lane === SyncLane) {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
resetRenderTimer();
// 同步执行
flushSyncCallbackQueue();
}
}
mostRecentlyUpdatedRoot = root;
}
React17 中如何实现异步
如上一点所示,对于判断是否是异步最关键的参数是executionContext字段。
所以,在正常调用setState过程中,有地方会对executionContext进行赋值,使它不等于NoContext。从而进入异步过程。
具体实现则在React的合成事件中。
合成事件
React通过“根节点事件代理”、“事件分发”、“原生事件对象封装”实现了“合成事件”。【网络上有很多关于合成事件的文章】。在具体执行用户绑定的事件回调方法前,其实已经在React内部执行了一系列的方法(如下图)。
try/final 和 事务机制
上图中,从其中的几个方法【discreteUpdates、batchedEventUpdates】里,可以清晰的看到,通过try/final 方法,在执行具体代码前后,都对executionContext进行了操作。所以当最外层事件触发方法的final方法时(此时executionContext === NoContext),执行flushSyncCallbackQueue方法,处理syncQueue队列里的回调,完成组件的re-render。
function discreteUpdates$1(fn, a, b, c, d) {
// 此时executionContext === 0
var prevExecutionContext = executionContext;
executionContext |= DiscreteEventContext;
{
try {
return runWithPriority$1(UserBlockingPriority$2, fn.bind(null, a, b, c, d));
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
// 清空syncQueue队列
flushSyncCallbackQueue();
}
}
}
}
五、re-render阶段renderRootSync和commitRoot的不同
当组件需要重新渲染时,大部分的逻辑同mount阶段是一致的。
其中最大的区别在于:
- 优先通过current Fiber.alternate创建workInProgress Fiber
- 在reconcileChildren时,会有diff current和workInProgress的逻辑
renderRootSync
复用current Fiber
function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
}
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
return workInProgress;
}
diff节点
reconcileSingleElement
通过child.key === key和child.elementType === element.type,来判断是否是同一个节点。
是同一个节点,则复用current Fiber,
不是同一个节点,则通过deleteRemainingChildren删除所有的children节点,并在之后通过createFiberFromElement方法创建新的workInProgress Fiber
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
switch (child.tag) {
case Block:
if (enableBlocksAPI) {
let type = element.type;
if (type.$$typeof === REACT_BLOCK_TYPE) {
// The new Block might not be initialized yet. We need to initialize
// it in case initializing it turns out it would match.
if (
((type: any): BlockComponent<any, any>)._render ===
(child.type: BlockComponent<any, any>)._render
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.type = type;
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
}
// We intentionally fallthrough here if enableBlocksAPI is not on.
// eslint-disable-next-lined no-fallthrough
default: {
if (
child.elementType === element.type
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
break;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, 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;
}
}
reconcileChildrenArray
childrenArray的diff逻辑,最主要是在2个for循环
循环1
依次对新旧节点进行比较。
在当前循环中,在updateSlot方法里判断是否是同一个节点,并返回新节点的Fiber。
- 如果newFiber和oldFiber都为null,则对比下一个兄弟节点
- 如果oldFiber存在,且newFiber是新建的(不复用oldFiber),表示不是同一个节点,则删除oldFiber
- 如果newFiber和oldFiber是同一个节点,则对比下一个兄弟节点(同时对newFiber通过next形成链表)
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 新建workInProgress Fiber
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);
// 对所有的newFiber形成链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
循环2
当还有新节点没创建时触发。
通过createChild创建Fiber,并插入到之前新建Fiber链表的尾部
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
完整代码
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;
// 循环1
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) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 当新节点都新建完后,删除旧节点,返回第一个新节点
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 循环2
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
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) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
commitRoot
commit阶段,最大的区别是在root.finishedWork。
mount阶段时,finishedWork指向根节点的child。而re-render阶段,finishedWork指向的是需要更新的Fiber,如果有多个Fiber需要更新,则通过nextEffect形成链表。
总结
- commit阶段,通过在reconcile阶段生成的root.finishedWork.firstEffect,通过commitMutationEffects方法一次对需要更新的Fiber进行DOM渲染。
- setState通过事务机制对executionContext进行赋值,并通过executionContext === NoContext 来判断当前是进行异步还是同步。
- diff逻辑
- 只比较同一层的节点。
- 通过key和elementType判断是否是同一个节点。不是则删除老节点新建新节点,是则复用老节点。
- diff多个子节点时,通过for循环从头至尾逐一进行比较(不像vue那样通过双指针来优化diff逻辑)。