0. 示例项目代码
function App() {
const [num, setNum] = useState(0)
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p onClick={() => setNum(num+1)}>
{num}
</p>
</header>
</div>
);
}
点击p标签后工作流程,打开chrome performance点击录制,
可以看到是从
flushSyncCallbacks开始,而这个方法执行的是performSyncWorkOnRoot
performSyncWorkOnRoot
由于之前的章节已经记录过就不再赘述
function performSyncWorkOnRoot(root: FiberRoot) {
// 进入渲染阶段
let exitStatus = renderRootSync(root, lanes);
}
renderRootSync
function renderRootSync(root, lanes)
{
prepareFreshStack(root, lanes);
// 工作循环
workLoopSync();
}
prepareFreshStack
React 18 中的一个新功能,它是为了优化 React 的更新和渲染过程,具体来说,它在 React 中引入了一种新的数据结构,称为“Stack”,以替代旧有的“Fiber”数据结构。
prepareFreshStack 这个函数的作用就是创建一个全新的、空的 Stack,以供 React 在更新和渲染时使用。具体来说,这个函数会做以下几件事情:
- 创建一个空的 Stack 对象。
- 将这个 Stack 对象传递给 React 的调度器(Scheduler),以供调度器在更新和渲染过程中使用。
- 重置一些全局状态,以确保 React 在使用新的 Stack 时能够正常工作。
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
const rootWorkInProgress = createWorkInProgress(root.current, null);
}
createWorkInProgress
创建一个 WIP 节点,以便在 WIP 树中描述这个组件的更新过程。然后,React 会基于 WIP 树中的节点执行更新,直到 WIP 树中所有节点的更新都已完成,最后再将更新结果同步到实际 DOM 树中,从而完成组件的更新过程。
function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 如果不为空,根据current的属性创建
workInProgress.type = current.type;
return workInProgressv
}
return 出来以后会返回到 prepareFreshStack 再到 renderRootSync 然后执行 workLoopSync
performUnitOfWork
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
套到上面的代码里逻辑如下
root节点进入beginWorkfunction Component App()进入beginWorkuseState节点进入beginWorkdiv节点进入beginWorkheader节点进入beginWorkimg节点进入beginWorkimg没有子节点 进入completeWorkimg兄弟节点p进入beginWorkp进入completeWork
1. root节点进入beginWork
beginWork
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
attemptEarlyBailoutIfNoScheduledUpdate
可以看到直接将当前 workInProgress 的stateNode 赋值给了root
var root = workInProgress.stateNode;
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
bailoutOnAlreadyFinishedWork
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
由此可知,如果fiber节点未改变,就会直接执行 cloneChildFibers 来创建子节点
2. app 进入beginWork
// 没有挂起的更新或上下文。退出
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
进入一个swith判断,通过查询ReactWorkTags可知,App()对应的tag为FunctionComponent = 0;
function attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
跟上面的逻辑一样,会进入cloneChild,根据current,生成WIP
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
) {
// 如果 props 或 context 改变了, 将该fiber机诶安标记为已完成工作
// 如果稍后确定props相等(备注),则可以取消设置。
didReceiveUpdate = true;
} else {
//props和legacy context都不会改变。检查是否有挂起的
//更新或上下文更改。
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
//如果这是错误或悬疑边界的第二次通过
//可能无法在“当前”上安排工作,因此我们检查此标志。
(workInProgress.flags & DidCapture) === NoFlags
) {
// 没有挂起的更新或上下文。退出
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// 这是一种仅适用于传统模式的特殊情况。
didReceiveUpdate = true;
} else {
//已计划对此fiber进行更新,但没有新props
//也不是legacy context。将此设置为false。如果更新队列或上下文
//消费者产生一个改变的值,它会将其设置为true。否则
//该组件将假设孩子没有改变并退出。
didReceiveUpdate = false;
}
}
}
}
2. p 进入beginWork
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
// 进入update逻辑
// p节点的子节点只有个number,所以 isDirectTextChild = true
if (isDirectTextChild) {
nextChildren = null;
}
// 所以 nextChildren = null, 开始执行completeWork的逻辑
updatePayload = prepareUpdate(instance, type, oldProps, newProps, currentHostContext)
export function prepareUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
hostContext: HostContext,
): null | Array<mixed> {
return diffProperties(domElement, type, oldProps, newProps);
}
3. diffProperties
在React中,当组件需要更新时,React使用一种称为“协调(reconciliation)”的算法来比较当前的虚拟DOM树和之前的虚拟DOM树的差异,并确定应该更新哪些部分的DOM树。其中一个重要的步骤是比较组件的属性(props)。
在比较属性时,React使用一个名为
diffProperties的函数。diffProperties函数接收两个参数:之前的属性对象和当前的属性对象。函数返回一个对象,该对象包含应添加、删除和更新的属性。
当React在进行属性比较时,它首先比较新旧属性对象中的key,如果key不同,React会认为这是不同的属性,会将旧属性全部替换为新属性。
如果新旧属性对象中有相同的key,React会比较它们的值,如果值相同,React不需要进行任何操作,否则React将把新值设置为DOM属性的新值。
此外,diffProperties函数还处理了一些特殊情况,例如style属性和事件处理程序,以确保它们被正确地添加、删除和更新。
// Calculate the diff between the two objects.
export function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): null | Array<mixed> {
if (__DEV__) {
validatePropertiesInDevelopment(tag, nextRawProps);
}
let updatePayload: null | Array<any> = null;
let lastProps: Object;
let nextProps: Object;
switch (tag) {
case 'input':
lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'select':
lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'textarea':
lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
default:
// p 标签进入的是这里
lastProps = lastRawProps; // {}
nextProps = nextRawProps;
if (
typeof lastProps.onClick !== 'function' &&
typeof nextProps.onClick === 'function'
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
assertValidProps(tag, nextProps);
let propKey;
let styleName;
let styleUpdates = null;
for (propKey in lastProps) {
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
continue;
}
if (propKey === STYLE) {
const lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = ({}: {[string]: $FlowFixMe});
}
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
// Noop. This is handled by the clear text mechanism.
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// Noop. It doesn't work on updates anyway.
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
if (!updatePayload) {
updatePayload = [];
}
} else {
// For all other deleted properties we add it to the queue. We use
// the allowed property list in the commit phase instead.
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
continue;
}
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
if (!styleUpdates) {
styleUpdates = ({}: {[string]: string});
}
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
if (!styleUpdates) {
styleUpdates = ({}: {[string]: $FlowFixMe});
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
}
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
const lastHtml = lastProp ? lastProp[HTML] : undefined;
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, nextHtml);
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string' || typeof nextProp === 'number') {
(updatePayload = updatePayload || []).push(propKey, '' + nextProp);
}
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
// We eagerly listen to this even though we haven't committed yet.
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
// that the "current" props pointer gets updated so we need a commit
// to update this element.
updatePayload = [];
}
} else {
// For any other property we always add it to the queue and then we
// filter it out using the allowed property list during the commit.
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
if (__DEV__) {
validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
}
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
// ['children', '4']
return updatePayload;
}