本文讲解[[react]]应用通过执行setState``useState
更新数据后,页面重新渲染的过程。
在初始化渲染完成之后,fiber树是这样的:
FiberRootNode
| ^
| |
current stateNode
| |
V |
hostRootFiber <-- alternate --> hostRootFiber
| ^
| |
child return
| |
V |
HostComponentFiber(div)<------------------------------+
| ^ ^
| | |
child return return
| | |
V | |
FunctionComponentFiber(FnComp) -- sibling -> ClassComponentFiber(ClazzComp)
| ^ | ^
| | | |
child return child return
| | | |
V | V |
HostComponentFiber(p) HostComponentFiber(p)
触发更新
// 还是之前的例子,类组件添加更新逻辑
<!DOCTYPE html>
<html lang="en">
<head>
<title>React App</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
import ReactDom from 'react-dom/client'
function FnComp(props) {
return <p>{props.children}</p>
}
import ReactDom from 'react-dom/client'
function FnComp(props) {
return <p>{props.children}</p>
}
class ClazzComp extends React.Component {
constructor(props) {
super(props);
this.state = { color: 'red' }
}
setCount = () => {
this.setState({ color: this.state.color === 'red' ? 'blue' : 'red' })
}
render() {
return <p style={{ color: this.state.color }} onClick={this.setCount}>{this.props.children}</p>
}
}
ReactDOM.createRoot(document.getElementById('container')).render(
<div id="root">
<FnComp>hello</FnComp>
<ClazzComp>react</ClazzComp>
</div>
);
Component.protoptype.setState = function(partialState, callback) {
// 这里的updater是在实例化类实例的时候添加的。
this.updater.enqueueSetState(this, partialState, callback);
}
function enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
// 创建一个update对象
const update = createUpdate();
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber)
}
function enqueueUpdate(fiber: Fiber, update) {
const updateQueue = fiber.updateQueue;
const sharedQueue = updateQueue.shared;
const pending = sharedQueue.pending;
// 将update对象串联到fiber.updateQueue.shared.pending上
if (pending === null) {
// 如果是空的,则直接赋值
update.next = update;
} else {
// 将update链接到当前pending的后一个
update.next = pending.next;
pending.next = update;
}
// pending指向最新的update,便于再次链接
sharedQueue.pending = update;
}
// 只有一个update对象时
pending--+
|
V
+--> update-1 --+
| |
+------next-----+
// 再添加一个update对象时
pending------------------------+
|
V
+--> update-1 -- next --> update-2 ---+
| |
+----------------next-------------------+
function scheduleUpdateOnFiber(
fiber: Fiber
) {
// 首先根据当前fiber节点拿到FiberRoot
const root = markUpdateLanFromFiberToRoot(fiber, lane);
// 让scheduler模块执行自己的回调
ensureRootIsScheduled(root);
}
function ensureRootIsScheduled(root: FiberRoot) {
// 让scheduler一段时间后回调performConcurrentWorkOnFiber,可以理解成setTimeout
// schedulerPriorityLevel表示优先级,不同的优先级表示不同的延迟时间
// performConcurrentWorkOnFiber在初始化的时候会调用renderRootSync
// 但是在重新渲染的时候也可能调用renderRootConcurrent
// 区别在于renderRootSync是不间断的调用performUnitOfWork
// renderRootConcurrent调用performUnitOfWork过程中可能中断、恢复
scheduleCallback(
shedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
准备阶段
// renderRootConcurrent会调用prepareFreshStack,依旧会创建workInProgress树
// 但这次是基于fiberRoot的current树,也是第一次构建的workInProgress树
workInProgress = createWorkInProgress(root.current);
function createWorkInProgress(current: Fiber) {
// 尝试复用alternate对象,没有的话才新建一个fiber对象
let workInProgress = current.alternate;
if (workInProgress === null) {
// 初次渲染,生成一个fiber对象
} else {
// 复用fier对象
// 重置一些属性
workInProgrese.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deleteions = null;
}
// child也复制了。
workInProgress.child = current.child;
// updateQueue对象也引用
// 这样workInProgress就可以根据update对象更新state了
workInProgress.updateQueue = current.updateQueue;
}
render阶段
beginWork
function beginWork(
current: Fiber,
workInProgress: Fiber
) {
if (current !== null) {
const oldProps = currentMemoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps === newProps) {
// props没有发生变化
// 检查当前fiber节点是否需要走更新流程。react通过lanes来判断是否需要更新,后续说明。
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext();
if (!hasScheduledUpdateOrContext) {
// 如果不需要更新,则跳过beginWork的过程,即updateXXXComponent这样的函数
// 在我们的例子中,下面组件会跳过更新
// HostRootFiber
// HostComponentFiber div#root
// FunctionComponentFiber FnComp
// 而ClassComponentFiber不会跳过更新,会执行updateClassComponent
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress);
}
}
}
switch(workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
return updateClassComponent(current, workInProgress, Component)
}
}
}
function updateClassComponent(
current: Fiber,
workInProgress: Fiber,
Component: any
) {
const instance = workInProgress.stateNode;
if (instance === null) {
// 这里是初次渲染分支:类组件实例化
constructClassComponent();
mountClassInstance();
} else if (current === null) {
// current是有值的,就是当前的渲染对应的fiber节点
} else {
// 更新流程走这里
shouldUpdate = updateClassInstance(current, workInProgress, Component);
}
const nextUnitOfWork = finishClassComponent(current, workInProgress, Component);
return nextUnitOfWork;
}
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
ctor: any
) {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps;
const oldState = workInProgress.memoizedState;
let newState = instance.state = oldState;
// 这里计算新的state,oldState+updateQueue 计算得到 newState
processUpdateQueue(workInProgress, newProps, instance);
newState = workInProgress.memoizedState;
if (oldState === newState) {
// 如果新旧state一样,就不用更新
return false;
}
// 这里就是调用shouldComponentUpdate,如果pureComponent,就是浅比较,默认返回true
const shouldUpdate = checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
oldState,
newState
)
// 将新的props state都添加到实例上,render函数才能访问得到
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
function finishClassComponent(
current: Fiber,
workInProgress: Fiber,
Component: any
) {
const instance = workInProgress.stateNode;
// 生成新的子虚拟dom,因为this.state已更新,所以nextChildren和初始化渲染时有不同。
// 我们的例子中将得到p对应的虚拟dom,且props包含style,置为{ color: 'blue' }
// 旧的props中的style是{ color: 'red' }
const nextChildren = instance.render();
// 调和
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child
}
// 初始化也会调用mountChildFibers调和,更新渲染会调用reconcileChildFibers调和
// 2个函数都是工厂函数返回的,所以核心逻辑是一样的
// export const reconcileChildFibers = ChildReconciler(true);
// export const mountChildFibers = ChildReconciler(false);
function reconcileChildren(current, workInProgress, nextChildren) {
if (current === null) {
// 初始化走这里
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren
)
} else {
// 再次渲染走这里
// reconcileChildFibers和mountChildFibers是同一个工厂函数返回的,区别就是是否置一些flags
// 也就是说2个函数就是同一函数。
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren
)
}
}
// 如果只有一个新的虚拟dom,就是我们的例子中的情况。
function reconcileSingleElement(
returnFiber: Fiber, // 当前的workInProgress
currentFirstChild: Fiber, // current.child
newChild: any // 新的虚拟dom
) {
if (typeof newChild === 'object' && newChild !== null) {
switch(newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return reconcileSingleElement(returnFiber, currentFirstChild, newChild);
}
}
}
// 只有一个新的虚拟dom,老的fiber对象不知道几个
// 所以需要尝试复用,且删除多余的fiber对象
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
const key = element.key;
let child = currentFirstChild;
while(child !== null) {
if (child.key === key) {
if (child.elementType === element.type) {
// 删除所有的兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 复用fiber,内部也是调用createWorkInProgress
// 我们的例子会走到这里,会复用p标签对应的fiber对象
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
}
// key一样,那是没有复用,则全部删除
deleteRemainingChildren(returnFiber, child);
} else {
// key不匹配则删除
deleteChild(returnFiber, child);
}
// 从前往后尝试复用
child = child.sibling;
}
// 走到这里是没能复用的情况:创建新的fiber节点
const created = createFiberFromElement(element, returnFIber.mode);
created.return = returnFiber;
return create;
}
completeWork
function updateHostComponent(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props
) {
// 计算得到新的props对象
const updatePayload = prepareUpdate(
workInProgress.stateNode,
type,
current.memoizedProps,
newProps
)
// render阶段不提交,等到commit阶段再提交
workInProgress.updateQueue = updatePayload;
}
render阶段结束后,内存中有2颗fiber树,current代表当前页面展示的树,workInProgress代表下次展示的树
同时同一层的对应的fiber节点之间是通过alternate相互引用的,如同2个hostRootFiber一样
以后createWorkInProgress就可以复用alternate的fiber对象。
FiberRootNode
| ^
| |
current stateNode workInProgress
| | |
V | V
hostRootFiber <-------- alternate --------> hostRootFiber
| ^ | ^
| | | |
child return child return
| | | |
V | V |
HostComponentFiber(div)<------------+ HostComponentFiber(div)<------------+
| ^ ^ | ^ ^
| | | | | |
child return return child return return
| | | | | |
V | | V | |
FunctionComponent -sibling-> ClassComponent FunctionComponent -sibling-> ClassComponent
| ^ | ^ | ^ | ^
| | | | | | | |
child return child return child return child return
| | | | | | | |
V | V | V | V |
HostComponent(p) HostComponent(p) HostComponent(p) HostComponent
commit阶段
function commitMutationEffectsEffectsOnFiber(
finishedWork: Fiber
) {
switch(finishedWork.tag) {
case HostComponent: {
const instance = finishedWork.stateNode;
if (instance !== null) {
const updatePayload = finishedWork.updateQueue;
finishedWork.updateQueue = null
if (updatePayload !== null) {
// 将render阶段的计算结果应用到instance上
commitUpdate(instance, updatePayload);
}
}
}
}
}
总结
- 类组件通过setState提交一个update对象,同样也需要经历准备阶段、render阶段、commit阶段,最终将修改应用到页面上
- 首次渲染,react会为所有组件(函数组件、类组件、host组件)创建对应的fiber对象;再次渲染时react会复用老的fiber对象。具体来说,初次渲染和第二次渲染都会创建新的fiber对象,之后都会复用老的fiber对象