你不知道的函数式组件在mount阶段的流程和其hook所执行的时机
提出问题
想必用过React的对UseState应该并不陌生,但是你知道其在mount阶段的执行的时机吗?
先思考一个问题:当我们在非FC(函数式组件)中使用hook时会出现什么情况呢?会出现如下的错误:
那么Hook本身作为一个函数是如何知道当前它的上下文的呢?React的解决方法就是:在不同上下文中所调用的Hook是不同的,并且实现了一个内部数据共享层。大致的情况如下:
还有一个问题是Hook数据保存在哪里,在类组件中我们知道保存在state中,而在FC中每次更新都是函数的重新执行,所以不能保存在函数里,那么只能保存在FC对应的fiberNode上,只要这个FC还存在那么这个fiberNode就不会销毁。其保存的形式大致如下:
mount的大致流程:
在这我们只考虑单节点,如下结构
typescript
function App() {
const [num] = useState(100);
return (
<div>
<sapn>{num}</sapn>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
调度准备阶段
在我们的React的项目中src文件夹下的index.js文件中会有如下代码
js
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
//index.html
<div id="root"></div>
这就是mount阶段的入口,我们看一下ReactDOM下createRoot函数所做的事情,下面为导出的函数:
typescript
export function createRoot(container: Container) {
const root = createContainer(container);
//...其他代码
return {
render(element: ReactElementType) {
return updateContainer(element, root);
}
};
}
这个container你可以想象成一个容器,即为传进来的div元素,createContainer函数所做的事情就是创建HostRootFiber和FiberRootNode,那么可能又有人会问HostRootFiber和FiberRootNode是什么呢?这个在这就不介绍过多,想要了解更多请查看别的资料,我就用一张图片来表示:
这个FiberRootNode用来切换current和workInProgress树来实现双缓冲树,这个HostRootFiber对应的即为root对应的element对应的Fiber。
可以看到返回一个对象其中包含一个render函数,其中又调用了updateContainer,这个updateContainer的作用大致就是为root添加一个Update,然后开启调度,记住这个hostRootFiber和updateQueue,后面要考哦。注意:我们在React提到的root对应的应该是FiberRootNode,而不是HostRootFiber。
typescript
export function updateContainer(
element: ReactElementType | null,
root: FiberRootNode
) {
const hostRootFiber = root.current;
//<App/>对应的element
const update = createUpdate<ReactElementType | null>(element);
enqueueUpdate(
hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
update
);
scheduleUpdateOnFiber(hostRootFiber);
return element;
}
调度阶段
在调度阶段我们会执行下列几项操作:
- 向上遍历到root,(这是mount阶段,所以hostRootFiber.stateNode即可,但在update中可能发生在任意一个fiber上)
- 调用renderRoot
- 初始化wip树
- beginWork阶段
- completeWork阶段
- commitWork阶段
- commit阶段
在这我们探讨FC的mount阶段,如果想了解其他类型的mount阶段可以关注后续发的文章。
向上遍历
typescript
function markUpdateFromFiberToRoot(fiber: FiberNode) {
let node = fiber;
let parent = node.return;
while (parent !== null) {
node = parent;
parent = node.return;
}
//hostRootFiber.tag = HostRoot
if (node.tag === HostRoot) {
return node.stateNode;
}
return null;
}
renderRoot函数
typescript
function renderRoot(root: FiberRootNode) {
//初始化
prepareFreshStack(root);
do {
try {
workLoop();
break;
} catch (e) {
if (__DEV__) {
console.warn('workLoop发生错误', e);
}
}
} while (true);
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
commitRoot(root);
}
初始化wip树
typescript
function prepareFreshStack(root: FiberRootNode) {
workInprogress = createWorkInProgress(root.current, {});
}
//createWorkInProgress
export const createWorkInProgress = (
current: FiberNode,
pendingProps: Props
): FiberNode => {
let wip = current.alternate;
if (wip === null) {
//mount
wip = new FiberNode(current.tag, pendingProps, current.key);
//其他代码
}
//其他代码
}
beginWork
beginWork的作用大致可以表达为:根据子的current fiberNode 和 reactElement,生成子对应的fiberNode,并且打上相对应的flags,然后返回子fiberNode,由于是mount阶段,只会有Placement Flag(插入)。
进入beginWork后根据不同的fiber.tag进入不同的流程:
typescript
export const beginWork = (wip: FiberNode) => {
//与React Element比较,生成FiberNode,然后再返回子FiberNode
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip);
case HostComponent:
return updateHostComponent(wip);
case HostText:
return null;
case FunctionComponent:
return updateFunctionComponent(wip);
default:
if (__DEV__) {
console.warn('workloop未实现的类型', wip);
}
break;
}
return null;
};
hostRootFiber beginWork------首屏渲染优化
第一个进入的当然是hostRootFiber,本来应该不在这提的,但是由于其中包含一个关于react首屏渲染的优化,所以还是在这里提一下,如果你已经了解了,那么可以直接跳到下一个fiberNode的beginWork中。
typescript
function updateHostRoot(wip: FiberNode) {
const baseState = wip.memoizedState;
const updateQueue = wip.updateQueue as UpdateQueue<Element>;
const pending = updateQueue.shared.pending;
updateQueue.shared.pending = null;
//memoizedState为传递进来的React Element,<APP/>组件
const { memoizedState } = processUpdateQueue(baseState, pending);
wip.memoizedState = memoizedState;
const nextChildren = wip.memoizedState;
reconcileChildren(wip, nextChildren);
return wip.child;
}
在updateContainer函数中我们已经为hostRootFiber创建了一个update,这个update就是来帮助我们创建App组件对应的fiberNode的,消费完这个update后就能获得到children的信息。还记得在上面让记住的hostRootFiber嘛,为什么我们要在未进行调度的时候就创建hostRootFiber并在它的updateQueue上添加一个update,这个就reconcileChildren函数有关了,别着急继续往下面看:
typescript
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
const current = wip.alternate;
if (current !== null) {
//update
wip.child = reconcileChildFibers(wip, current?.child, children);
} else {
//mount
wip.child = mountChildFibers(wip, null, children);
}
}
typescript
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
这两个函数都是由ChildReconciler函数返回的,唯一的不同点在于shouldTrackEffects:是否追踪副作用这个参数的不同,区分进入不同树的依据就是wip.alternate是否存在这个fiber.alternate就是current树上的fiberNode,两棵树的fiberNode通过alternate连接,即wip.alternate = currentFiberNode,currentFiberNode.alternate = wip。这就是我们在调度之前创建hostRootFiber的原因,这样就能只让hostRootFiber进入reconcileChildFibers函数中。因为只有hostRootFiber.alternate !== null。
那么区分hostRootFiber和其他fiber有什么用呢?还记得为什么要提hostRootFiber的beginWork流程吗?对,首屏渲染。因为是mount阶段不像后续的update阶段只需更新部分fiberNode的状态,而在这我们需要创建fiberNode,并且将它们一个一个插在父fiberNode中去,如果首屏渲染的组件很多那么就要消费很多Placement flag,所以为了解决首屏渲染的速度问题,我们可以先构建一棵离屏的DOM树,在都构建完成之后,一并插入到hostRootFiber中去,这时我们仅仅需要消费一个Placement flag。到这是不是很清晰了,我们需要打上Flag的fiberNode正是App组件所对应的fiberNode中,下面来看一下具体的实现:
typescript
function placeSingleChild(fiber: FiberNode) {
if (shouldTrackEffects && fiber.alternate === null) {
fiber.flags |= Placement;
}
return fiber;
}
因为我们在调度之前只创建了hostRootFiber和fiberRootNode,所以App对应的current树下的fiberNode是不存在的,所以会打上Placement。
完整的ChildReconciler函数,有兴趣的可以看一下,有什么不明白的可以提问:
typescript
function ChildReconciler(shouldTrackEffects: boolean) {
function reconcileSingleElement(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
element: ReactElementType
) {
//根据ReactElement创建一个fiber然后返回
const fiber = createFiberFromElement(element);
fiber.return = returnFiber;
return fiber;
}
function reconcileSingeTextNode(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
content: string | number
) {
const fiber = new FiberNode(HostText, { content }, null);
fiber.return = returnFiber;
return fiber;
}
//插入单一的节点
function placeSingleChild(fiber: FiberNode) {
if (shouldTrackEffects && fiber.alternate === null) {
fiber.flags |= Placement;
}
return fiber;
}
return function reconcileChildFibers(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
newChild?: ReactElementType
) {
//判断当前fiber的类型
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFiber, newChild)
);
default:
if (__DEV__) {
console.warn('未实现的reconcile类型', newChild);
}
}
}
//HostText
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingeTextNode(returnFiber, currentFiber, newChild)
);
}
if (__DEV__) {
console.warn('未实现的reconcile类型', newChild);
}
return null;
};
//其他代码
}
FC beginWork
接下来就要进入我们的FC beginWork了,接下来我们要进入这个函数updateFunctionComponent。这时候想想FC的结构,jsx放在return中,那么我们要创建子fiberNode肯定要拿到其return出来的结果就是nextChildren并处理。那么是不是拿到这个函数并且执行是不是就可以了,这个函数就该fiber对应的type上:
拿到这个函数是不是就可以开始执行了,传入相对应的props,函数开始执行,碰到useState。在开始后续的步骤时先提几个概念,
typescript
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
currentDispatcher
};
这个 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED就是前面提到的内部数据共享层,其中包括了一个currentDispatcher,这个就相当于上面那张图中的当前可使用的hook集合。currentDispatcher的信息:
typescript
const currentDispatcher: { current: Dispatcher | null } = {
current: null
};
我们在使用react的hook时都知道从react这个包中引入,那么在react包中暴露出去的hook:
typescript
export const useState: Dispatcher['useState'] = (initialState) => {
//获取当前上下文中所以的hook,并从中拿到useState
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
};
export const resolveDispatcher = (): Dispatcher => {
const dispatcher = currentDispatcher.current;
if (dispatcher === null) {
throw new Error('hook只能在函数式组件中执行');
}
return dispatcher;
};
先导致了解一下这些概念,没理解为什么要这么做也没关系,后续会再捋一遍。
但是我们的调度在react-reconciler中,所以这个数据共享层的作用就体现出来了,我们可以在react-reconciler中使用这个东西了。
回到我们的updateFunctionComponent函数,其内部通过调用renderWithHooks函数获得其nextChildren,看一下nextChildren函数:
typescript
let currentlyRenderingFiber: FiberNode | null = null;//当前真正调度的wip
let workInProgressHook: Hook | null = null;//当前正在执行的hook
export function renderWithHooks(wip: FiberNode) {
//赋值
currentlyRenderingFiber = wip;
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
//update
} else {
//mount
//mount时的hook
currentDispatcher.current = HooksDispatcherOnMount;
}
//函数式组件的函数保存在该对应fiber的type上
const Component = wip.type;
const props = wip.pendingProps;
const children = Component(props);
//重置
currentlyRenderingFiber = null;
return children;
}
这个流程相信大家应该都不陌生了,当前传进来的是FC对应的fiberNode,wip.alternate为null,这时候将 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED中的currentDispatcher.current进行赋值,HooksDispatcherOnMount就对应mount时的hook集合,我们继续在下面实现这个current
typescript
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState
};
这样是不是就能理解了react的hook为什么能感知上下文了吧。
捋一遍逻辑:
当我们在FC中使用hook时,实际导入的是react包下的hook,但是我们并没有在那里实现,而是将它指向__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.currentDispatcher.current,我们的实现是在这个FC进入beginWork时,更具体来说是进入renderWithHooks,我们在这个函数中给currentDispatcher.current赋值,所以当解析到hook时,找到我们在react包中暴露出去的hook,而那个hook又指向的是当前currentDispatcher.current下的hook,也就是mountState,所以最后执行的是mountState。还有就是当我们在函数式组件外使用hook时,发现当前的currentlyRenderingFiber是没有赋值的(进入函数式组件才会赋值),这时候就会报hook只能在FC中使用的错误,当当前的currentDispatcher.current是有值时我们即可将currentDispatcher.current指向一个全是报错的地方,调用任何一个hook都抛出一个错误,这就是hook中不能使用hook的实现。
继续往下走就是执行useState,这个没什么说的,直接上代码:
typescript
function mountState<State>(
initialState: (() => State) | State
): [State, Dispatch<State>] {
//找到当前useState对应的hook数据
const hook = mountWorkInProgresHook();//获取当前的hook数据,创建hook,形成单向链表
let memoizedState = null;
if (initialState instanceof Function) {
memoizedState = initialState();
} else {
memoizedState = initialState;
}
const queue = createUpdateQueue<State>();
hook.updateQueue = queue;
hook.memoizedState = memoizedState;
// @ts-ignore
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
queue.dipatch = dispatch;
return [memoizedState, dispatch];
}
function dispatchSetState<State>(
fiber: FiberNode,
updateQueue: UpdateQueue<State>,
action: Action<State>
) {
const update = createUpdate(action);
enqueueUpdate(updateQueue, update);
scheduleUpdateOnFiber(fiber);
}
在这里值得一提的是:
typescript
function App() {
const [num] = useState(0);
return (
<div>
<sapn>{num}</sapn>
</div>
);
}
你觉得这个div和span哪个先进入jsx方法,也就是createElement方法,答案是span,有兴趣的可以打一个断点去看看。
接下来就要进入return出的nextChildren的beginWork,一直到HostText,然后再进行completeWork。
completeWork
这个阶段就是根据beginWork生成的fiberNode,自下而上的去创建宿主中的实例,在浏览器中就是element,这个使用的是浏览器提供的方法来创建,创建完成之后插入到其父element中去。在这个期间还会进行flags冒泡,什么是flags冒泡呢?在beginWork中我们为部分fiberNode打上了flag,我们将这个子孙的flag保存在父fiberNode中。有人可能就会问,又进行递归会不会太消耗性能?别忘了,我们现在处在completeWork下,是从下到上的,刚好能够将flags带上去,flags冒泡函数:
typescript
function bubbleProperties(wip: FiberNode) {
let subtreeFlags = NoFlags;
let child = wip.child;
while (child !== null) {
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child.return = wip;
child = child.sibling;
}
wip.subtreeFlags = subtreeFlags;
}
这个 subtreeFlags保存的就是当前fiberNode子孙fiberNode所包含的flags(不包含自己的flags)。
当completeWork结束后,一颗离屏的DOM树就构建好了,flags也冒泡到了hostRootFiber。
commit阶段
commit阶段其实和这个mount时期的FC已经没有很大的关系了,commit阶段主要就是向下遍历找到flags并消费,在这里也就是Placement,这样调度的流程就结束了。
总结
没啥总结的,连续敲了近3个小时,如果有错误请各位大佬指点,如果觉得写得还行的点点赞哦,感谢!!!
本文章中出现的所有代码皆是我自己实现的react(未实现完)中拷贝出来的,能通过官方的测试样例请放心,项目地址:github.com/samllbin/My...